如何使用其REST API在Android上的Google云端硬盘上访问应用程序数据

时间:2019-01-05 12:57:11

标签: android kotlin google-drive-api

Google正在搁置其用于访问Google服务(即Google云端硬盘)的Android API,并将其替换为REST。

尽管有一个“迁移指南”,但由于“重复类定义”之类的原因,它无法构建可供安装的APK软件包。

由于某种原因,很难找到一些有关如何通过Android使用REST(最好是使用操作系统固有的方法)访问Google服务的全面信息。

1 个答案:

答案 0 :(得分:1)

经过大量的搜索,困惑,挠头,偶尔的咒骂以及对我真正不想关心的事情的大量学习之后,我想分享一些实际上有效的代码对我来说。

免责声明:我是一名菜鸟Android程序员(他的确不怎么挑剔),因此,如果这里有些事情让真正的Android向导摇摇头,希望您能原谅我

所有代码示例均使用Kotlin和Android Studio编写。

值得注意的是:在本小教程中仅查询“应用程序数据文件夹”,如果要执行其他操作,则需要调整请求的scopes

必要的准备工作

按照here所述为您的应用程序创建一个项目和一个OAuth密钥。我收集的许多授权信息都来自那个地方,所以希望能找到一些相似之处。

您的项目的信息中心可能位于https://console.developers.google.com/apis/dashboard

implementation "com.google.android.gms:play-services-auth:16.0.1"添加到应用程序gradle文件中。此依赖关系将用于身份验证。

为您的应用程序清单添加“互联网”支持

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

认证

我们旅程的开始是认证。 为此,我使用了GoogleSignIn框架。

创建一个活动(或使用您的主要活动,您的选择)并在那里覆盖onActivityResult方法。

添加这样的块:

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是在随播对象中定义为常量的任意选择的ID值。

一旦要执行身份验证(即通过单击按钮),就需要启动我们刚刚为其声明回调的活动。

为此,您需要先准备身份验证请求。

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

此请求为您提供了一个客户端对象,您可以通过调用直接使用该对象。

startActivityForResult(client.signInIntent, RC_SIGN_IN)

此呼叫将导致授权屏幕弹出(如有必要),允许用户选择一个帐户,然后再次关闭自身,将数据传递到onActivityResult

要获取以前登录的用户(不启动新活动),还可以在后台使用GoogleSignIn.getLastSignedInAccount(this);方法。

一旦失败,这两种方法都将返回null,因此请准备好处理。

现在我们拥有一个经过身份验证的用户,我们该如何处理?

我们要求提供身份验证令牌。 现在,我们的帐户对象中只有一个idToken,这对我们想做的事情绝对没有用,因为它不允许我们调用API。

但是Google再次提供了帮助,并向我们提供了GoogleAuthUtil.getToken(this, account.account, "oauth2:https://www.googleapis.com/auth/drive.appdata")通话。

此调用将转发帐户信息,如果一切顺利,则返回一个String:我们需要的身份验证令牌。

请注意:此方法执行网络请求,这意味着如果您尝试在UI线程中执行该请求,它将在您的脸上抛出。

我创建了一个帮助程序类,该类模仿Google的“任务”对象的行为(和API),该类解决了在线程上调用方法并通知调用线程已完成的麻烦。

将auth令牌保存在可以再次找到的位置,(最终)完成授权。

查询API

这部分比上一部分简单得多,并且与the Google Drive REST API并存

所有网络请求都需要在“非UI”线程上执行,这就是为什么我将它们包装在助手类中以在有数据显示时通知我的原因。

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
}

此代码块是我从各种来源整理来的相当通用的辅助函数,实际上只是使用URL进行查询,即执行的方法(GETPOSTPATCHDELETE)并从中构造一个HTTP请求。

我们在授权过程中获得的auth令牌作为标头传递给请求,以进行身份​​验证并将自己标识为Google的“用户”。

如果一切正常,Google会以HTTP_OK(200)进行回复,然后会调用onSuccess,它将把JSON回复转换为JSONObject,然后将其传递给我们之前注册的评估函数。

获取文件列表

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

spaces参数用于告知Google,我们不想看到根文件夹,而希望看到应用程序数据文件夹。没有此参数,请求将失败,因为我们只请求访问appDataFolder。

响应应在JSONArray键下包含一个files,然后您可以解析并绘制所需的任何信息。

ThreadTask类

此帮助器类封装了在不同上下文上执行操作并在完成时在实例化线程上执行回调的必要步骤。

我并不是说这是解决问题的方法,而只是我的“简直不知道”。

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
    }
}

在这种情况下,执行顺序很重要。 首先,您需要将回调添加到类对象,最后需要调用execute并向其提供您要使用其运行线程的执行器,当然还要提供要执行的代码。

这不是您使用Google云端硬盘可以做的所有事情,但这只是一个开始,我希望这个小小的汇编将来可以为其他人省去一些麻烦。