简体   繁体   中英

How to access the application data on Google Drive on Android with its REST API

Google is putting its Android API for accessing Google services (iE Google Drive) to rest and is replacing it with REST.

And while there is a 'migration guides', it fails to build a APK package ready for installation, because of 'Duplicate Class definition' or something.

For some reason it is incredibly hard to find some comprehensive information about how to access a Google Service using REST via Android (preferably using methods natively available to the OS).

After a lot of searching, puzzling, scratching my head, occasional swearing and a lot of learning about things I really didn't want to care about, I'd like to share a few pieces of code, that are actually working for me.

Disclaimer: I'm a rookie Android programmer (who really doesn't how to pick his battles), so if there are things in here, that have the real Android wizards shaking their heads, I hope you'll forgive me.

All code samples are written in Kotlin and Android Studio.

Worth noting: Only the 'application data folder' is queried in this little tutorial, you will need to adjust the requested scopes if you want to do something else.

Necessary preparations

Create a project and an OAuth key for your application as described here . Many of the information I gathered for authorization came from that place, so expect to find some similarities.

The Dashboard for your project may be found at https://console.developers.google.com/apis/dashboard

Add implementation "com.google.android.gms:play-services-auth:16.0.1" to your applications gradle file. This dependency will be used for authentication purposes.

Add 'internet' support to your applications manifest

<uses-permission android:name="android.permission.INTERNET"/>

Authenticating

The beginning of our journey is the authentication. For this purpose, I used the GoogleSignIn Framework.

Create an activity (or use your main activity, your choice) and override the onActivityResult method there.

Add a block like this:

if (requestCode == RC_SIGN_IN) {
    GoogleSignIn.getSignedInAccountFromIntent(data)
        .addOnSuccessListener(::evaluateResponse)
        .addOnFailureListener { e ->
            Log.w(RecipeList.TAG, "signInResult:failed =" + e.toString())
            evaluateResponse(null)
        }
}

RC_REQUEST_CODE is an arbitrarily chosen ID value defined in the companion object as constant.

Once you want to perform authentication (iE by clicking of a button), you will need to start the activity we have just declared the callback for.

For this purpose, you need to prepare the authentication request first.

GoogleSignIn.getClient(this, GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestIdToken("YourClientIDGoesHere.apps.googleusercontent.com")
        .requestScopes(Scope(Scopes.DRIVE_APPFOLDER))
        .build())

This request gives you a client object you can start using straight away by calling.

startActivityForResult(client.signInIntent, RC_SIGN_IN)

This call will cause the authorization screen to pop up (if necessary), allow the user to select an account and then close itself again, passing the data to onActivityResult

To fetch the previously signed in user (without starting a new activity), you can also use the GoogleSignIn.getLastSignedInAccount(this); method in the background.

On failure either of these methods return null , so be ready to deal with that.

Now that we have an authenticated user, what do we do with it?

We ask for an auth token. Right now, we only have an idToken in our account object, which is absolutely useless for what we want to do, because it doesn't allow us to call the API.

But Google comes to the rescue once more and supplies us with the GoogleAuthUtil.getToken(this, account.account, "oauth2:https://www.googleapis.com/auth/drive.appdata") call.

This call will forward the account information and return a String if all goes right: The auth token we need.

To be noted: This method performs a network request, meaning that it will throw up in your face, if you attempt to execute it in your UI thread.

I created a helper class which mimics the behavior (and API) of Googles 'Task' object, which takes care of the nitty gritty of calling a method on a thread and notifying the calling thread that it is done.

Save the auth token somewhere you can find it again, authorization is (finally) done with.

Querying the API

This part is far more straightforward than the previous one and goes hand in hand with the Google Drive REST API

All network requests need to be executed on a 'non-UI' thread, which is why I wrapped them up in my helper class to notify me once there is data to display.

private fun performNet(url: String, method: String, onSuccess: (JSONObject) -> Unit)
{
    ThreadedTask<String>()
        .addOnSuccess { onSuccess(JSONObject(it))               }
        .addOnFailure { Log.w("DriveSync", "Sync failure $it")  }
        .execute(executor) {
            val url = URL(url)
            with (url.openConnection() as HttpURLConnection)
            {
                requestMethod = method
                useCaches   = false
                doInput     = true
                doOutput    = false
                setRequestProperty("Authorization", "Bearer $authToken")

                processNetResponse(responseCode, this)
            }
        }
}

private fun processNetResponse(responseCode: Int, connection: HttpURLConnection) : String
{
    var responseData = "No Data"
    val requestOK    = (responseCode == HttpURLConnection.HTTP_OK)

    BufferedReader(InputStreamReader(if (requestOK) connection.inputStream else connection.errorStream))
        .use {
            val response = StringBuffer()

            var inputLine = it.readLine()
            while (inputLine != null) {
                response.append(inputLine)
                inputLine = it.readLine()
            }
            responseData = response.toString()
        }

    if (!requestOK)
        throw Exception("Bad request: $responseCode ($responseData)")

    return responseData
}

This block of code is a rather generic helper function I put together from various sources and essentially just takes the URL to query, the method to perform ( GET , POST , PATCH , DELETE ) and constructs a HTTP request from it.

The auth token we got earlier during the authorization is passed as a header to the request to authenticate and identify ourselves as 'the user' to Google.

Google will, if everything is OK, reply with HTTP_OK (200) and onSuccess will be called, which will translate the JSON reply to a JSONObject, which will then be passed to the evaluation function we registered earlier.

Fetching the list of files

performNet("https://www.googleapis.com/drive/v3/files?spaces=appDataFolder", "GET")

The spaces parameter serves to tell Google, that we don't want to see the root folder but the application data folder. Without this parameter, the request would fail, because we only requested access to the appDataFolder.

The response should contain a JSONArray under the files key, which you then can parse and draw whatever information you want.

The ThreadTask class

This helper class encapsulates the steps necessary to perform an operation on a different context and perform a callback on the instantiating thread upon completion.

I am not claiming that this is THE way to this, it's just my 'Simply doesn't know any better'-way.

import android.os.Handler
import android.os.Looper
import android.os.Message
import java.lang.Exception
import java.util.concurrent.Executor

class ThreadedTask<T> {
    private val onSuccess = mutableListOf<(T) -> Unit>()
    private val onFailure = mutableListOf<(String) -> Unit>()
    private val onComplete = mutableListOf<() -> Unit>()

    fun addOnSuccess(handler: (T) -> Unit)      : ThreadedTask<T> { onSuccess.add(handler); return this; }
    fun addOnFailure(handler: (String) -> Unit) : ThreadedTask<T> { onFailure.add(handler); return this; }
    fun addOnComplete(handler: () -> Unit)      : ThreadedTask<T> { onComplete.add(handler);return this; }

    /**
     * Performs the passed code in a threaded context and executes Success/Failure/Complete handler respectively on the calling thread.
     * If any (uncaught) exception is triggered, the task is considered 'failed'.
     * Call this method last in the chain to avoid race conditions while adding the handlers.
     *
     */
    fun execute(executor: Executor, code: () -> T)
    {
        val handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                publishResult(msg.what, msg.obj)
            }
        }

        executor.execute {
            try {
                handler.obtainMessage(TASK_SUCCESS, code()).sendToTarget()
            } catch (exception: Exception) {
                handler.obtainMessage(TASK_FAILED, exception.toString()).sendToTarget()
            }
        }
    }

    private fun publishResult(returnCode: Int, returnValue: Any)
    {
        if (returnCode == TASK_FAILED)
            onFailure.forEach { it(returnValue as String) }
        else
            onSuccess.forEach { it(returnValue as T) }
        onComplete.forEach { it() }

        // Removes all handlers, cleaning up potential retain cycles.
        onFailure.clear()
        onSuccess.clear()
        onComplete.clear()
    }

    companion object {
        private const val TASK_SUCCESS = 0
        private const val TASK_FAILED  = 1
    }
}

The order of execution is important in this case. You first need to add the callbacks to the class object and at the end you need to call execute and supply it with the executor you want to run the thread with and of course the code you want to execute.

It is not everything you can do with Google Drive, but it's a start and I hope this little compilation will save someone else some grief in the future.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM