Android App 存取 Google 雲端硬碟(Google Drive)

如果 Android App 有資料備份的需求,可以考慮使用 Google 雲端硬碟的服務。本篇文章會簡單說明一下實作必要流程與介紹官方工具的使用。

登入 Google 帳號

在使用所有 Google 服務前,都需要先讓 App 登入 Google 帳號並取得該服務相關權限。

首先確認一下 Android Studio 專案根目錄下的 build.gradle 中有沒有加入 Google 的 repository,照理說近期新版本 Android Studio 的新專案都會自動包含了。

allprojects {
repositories {
google()
// If you're using a version of Gradle lower than 4.1, you must instead use:
// maven {
// url 'https://maven.google.com'
// }
}
}

然後在 /project_name/app 目錄下的 build.gradle 中加入 Google Play services 和 http 的依賴函式庫。

apply plugin: 'com.android.application'
...
dependencies {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.google.http-client:google-http-client-gson:1.26.0'
implementation 'com.google.api-client:google-api-client-android:1.26.0'
implementation 'com.google.apis:google-api-services-drive:v3-rev136-1.25.0'
}

接著我們要取得 Google API 的 Token。
一般來說,Google Developers Console 中可以設定所有使用 Google 服務需要的 Token 和 API key,但第一次使用不免覺得有點複雜。所幸官方教學指南中有一個快速產生 Token 的方法:

點擊畫面中的按鈕,新增專案或選擇舊專案,建議一個 App 一個專案比較好管理。



選好專案後,上方選擇「Android」,下方的 Package name 就是你的 App 的 package name;SHA-1 signing certificate 則可以用 Java 的 keytool 指令產生,也可以用 Android Studio 的工具幫忙產生。


用 Android Studio 的工具產生 SHA-1 signing certificate:

1. 點擊右方 Gradle 工具
2. 雙擊 (ProjectName)/:app/Tasks/android/signingReport


3. 從下方的 Run 工具中就可以看到產生好的 SHA-1


4. 把資料都填好送出後就可以得到 Client ID 了


5. 最後還有一點是不要忘了在 Google Developers Console 啟用 Google Drive API。
    點選步驟:畫面左邊的資料庫 → 找到 Google Drive API → 啟用



再來就是程式碼的部份,主要邏輯如下:
  1. 使用 GoogleSignInOptions 設定向 Google 請求登入後要使用的服務。其中 requestScopes 設定服務類型,requestIdToken 就填入剛才申請的 Client ID。
  2. 宣告 GoogleSignInClient,給予 GoogleSignInOptions。
  3. 用 startActivityForResult 發出 GoogleSignInClient 的 SignInIntent。
  4. 接著畫面會跳出讓使用者同意授權的 Dialog,使用者操作完後就會回到 onActivityResult 方法去,我們就可以從回傳的 Intent data 中得到 GoogleSignInAccount。
  5. 從 GoogleSignInAccount 中取得已經授予權限的 Account 和憑證,並宣告 Drive 物件。
  6. 將 Drive 物件存到另一個自定義的 class DriveServiceHelper,我們可以在這個 class 中撰寫對雲端硬碟的存取操作。

private void requestSignIn() {
GoogleSignInOptions signInOptions =
new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(new Scope(DriveScopes.DRIVE_FILE),
new Scope(DriveScopes.DRIVE_APPDATA))
.requestIdToken("722xxxxxxxxxxxxxxxxxxxxxxxxxxxxx9ks.apps.googleusercontent.com")
.build();
GoogleSignInClient client = GoogleSignIn.getClient(getActivity(), signInOptions);
// The result of the sign-in Intent is handled in onActivityResult.
startActivityForResult(client.getSignInIntent(), REQUEST_CODE_SIGN_IN);
}
view raw Activity_1.java hosted with ❤ by GitHub
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_SIGN_IN:
handleSignInResult(data);
break;
default:
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
private void handleSignInResult(Intent result) {
GoogleSignIn.getSignedInAccountFromIntent(result)
.addOnSuccessListener(googleAccount -> {
Log.d(TAG, "Signed in as " + googleAccount.getEmail());
// Use the authenticated account to sign in to the Drive service.
GoogleAccountCredential credential =
GoogleAccountCredential.usingOAuth2(
getActivity(), Collections.singleton(DriveScopes.DRIVE_FILE));
credential.setSelectedAccount(googleAccount.getAccount());
Drive googleDriveService =
new Drive.Builder(
AndroidHttp.newCompatibleTransport(),
new GsonFactory(),
credential)
.setApplicationName("AppName")
.build();
// The DriveServiceHelper encapsulates all REST API and SAF functionality.
// Its instantiation is required before handling any onClick actions.
mDriveServiceHelper = new DriveServiceHelper(googleDriveService);
})
.addOnFailureListener(exception -> {
Log.e(TAG, "Unable to sign in.", exception);
});
}
view raw Activity_2.java hosted with ❤ by GitHub


Google Drive REST API

Google 原本有提供 Android 專用的 Google Drive Android API,但它現在已經屬於棄用(deprecated)的狀態,並將在 2019年12月6日 被關閉,因此我們只能使用 Google Drive REST API。

開始寫程式前先簡單講一下 Google Drive 的檔案架構。
Google Drive 檔案放置的區域分為 Drive、AppDataFolder、Photos 三部份。
  • Drive:就是一般我們從瀏覽器或 Drive App 登入雲端硬碟後可以隨意存取的區域。
  • AppDataFolder:登入雲端硬碟的 App 的專用區域,只有該 App 可以存取。
  • Photos:Google 相簿空間(待確定)

從前面的登入範例中我們在 GoogleSignInOptions 內指定的存取範圍「DRIVE_FILE」和「DRIVE_APPDATA」就分別代表 Drive 和 AppDataFolder。

private void requestSignIn() {
GoogleSignInOptions signInOptions =
new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestScopes(new Scope(DriveScopes.DRIVE_FILE),
new Scope(DriveScopes.DRIVE_APPDATA))
.requestIdToken("722xxxxxxxxxxxxxxxxxxxxxxxxxxxxx9ks.apps.googleusercontent.com")
.build();
GoogleSignInClient client = GoogleSignIn.getClient(getActivity(), signInOptions);
// The result of the sign-in Intent is handled in onActivityResult.
startActivityForResult(client.getSignInIntent(), REQUEST_CODE_SIGN_IN);
}
view raw Activity_1.java hosted with ❤ by GitHub

而在 Drive 上的檔案則有下列特性:
  • 檔案和資料夾目錄都被視為一個「file」,有它自己唯一的 File ID。
  • 資料夾的 MIME type 為「application/vnd.google-apps.folder」。
  • file 含有「parents」屬性,代表這個 file 存在哪些資料夾內。
  • Drive 和 AppDataFolder 根目錄也有自己的 File ID。
有了以上概念後,先來看看如何將檔案上傳。

上傳檔案

如果不指定 parents 的話,預設上傳到 Drive 根目錄。

File fileMetadata = new File();
fileMetadata.setName("photo.jpg");
java.io.File filePath = new java.io.File("files/photo.jpg");
FileContent mediaContent = new FileContent("image/jpeg", filePath);
File file = driveService.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute();
System.out.println("File ID: " + file.getId());

如果想上傳到 AppDataFolder,則設定 parents 為 "appDataFolder"。當然也可以設定多個 parents。

File fileMetadata = new File();
fileMetadata.setName("config.json");
fileMetadata.setParents(Collections.singletonList("appDataFolder"));
java.io.File filePath = new java.io.File("files/config.json");
FileContent mediaContent = new FileContent("application/json", filePath);
File file = driveService.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute();
System.out.println("File ID: " + file.getId());

尋找檔案

剛才上傳檔案的時候,我們取得了檔案的 File ID,如果有事先儲存起來的話,直接拿來指定是最快的。

driveService.files().get(fileId)
.setFields("id, name, mimeType, parents")
.execute()

如果沒有 File ID 也沒關係,我們可以指定搜尋條件來尋找檔案。搜尋的條件邏輯非常多,詳情請參考官方指南

driveService.files().list()
.setSpaces("appDataFolder")
.setFields("nextPageToken, files(id, name, mimeType, parents)")
.setPageSize(10)
.setQ("name = MyFile.txt")
.setOrderBy("modifiedTime desc")
.execute()

其他

其他像更新、下載、刪除......等功能都大同小異,只要考慮如何設定相關參數來達到你想要的存取效果就可以了。
Google 也提供了線上 API 參考及測試工具,方便開發者嘗試出符合需求的 API 參數設定。
https://developers.google.com/drive/api/v3/reference/

只要點擊下圖紅框處即可開啟 API 工具,填入各式參數去測試你的想法。

參考資料

https://developers.google.com/android/guides/setup
https://developers.google.com/identity/sign-in/android/start-integrating
https://developers.google.com/drive/api/v3/manage-uploads
https://github.com/gsuitedevs/android-samples/tree/master/drive/deprecation

留言

  1. 請問我再匯入下方這段指令時,
    dependencies {
    implementation 'com.google.android.gms:play-services-auth:16.0.1'
    implementation 'com.google.http-client:google-http-client-gson:1.26.0'
    implementation 'com.google.api-client:google-api-client-android:1.26.0'
    implementation 'com.google.apis:google-api-services-drive:v3-rev136-1.25.0'
    }

    為什麼程式原本有的這段指令implementation 'com.android.support:appcompat-v7:28.0.0' 會出現錯誤 ?

    回覆刪除
    回覆
    1. 可以先把滑鼠移到標紅線處看看錯誤訊息,或者嘗試編譯後看底下 Build 視窗的錯誤訊息喔

      刪除

張貼留言

這個網誌中的熱門文章

Android 藍牙連接通訊實作心得

在 Android 上自訂 Zxing 掃描框樣式與大小位置