💾

AndroidアプリからGoogle Driveにファイルをバックアップする

2021/05/03に公開約6,000字8件のコメント

この記事では、デベロッパーが作成したAndroidアプリからGoogle Driveにファイルをバックアップする機能の実装を解説していきます。

Google Cloud PlatformでGoogle Drive APIを有効化

Google Cloud Platformにログインして、Google Drive APIを有効化します。まだ、プロジェクトを作成していない場合は、作成してください。

左側のメニューから「APIとサービス>ダッシュボード」を選択します。

少しスクロールするとGoogle Drive APIのタブが出てきます。

OAuth同意画面の作成

左側のメニューから「OAuth同意画面」をクリックすると、設定画面が表示されるので色々と入力していきます。

スコープの画面で、「スコープを追加または削除」をクリックします。

../auth/driveに✓を入れます。

なお、../auth/driveのような機密性の高いスコープを設定した状態で本番環境にする場合は、Googleからの審査が必要です。その際、アプリケーションのホームページやプライバシーポリシーの用意が必須です。一方、非機密のスコープのみ使用する場合は審査が不要です。アプリ独自の設定データや特定のファイルのみ参照・編集する場合は.../auth/drive.appdata../auth/drive.fileのほうが審査の必要がないためお勧めです。

認証情報の作成

左側のメニューから認証情報をクリックし、上部の「認証情報を作成>OAuthクライアントID」を選択します。

ここでも各種情報を入力します。

SHA-1 証明書フィンガープリントは、以下のコマンドで確認することが出来ます。
keytool -list -keystore YOUR_KEYSTORE_PATH -v

Androidアプリへの実装

まず、moduleレベルのbuild.gradleに関連ライブラリを導入します。

dependencies {
	...
	implementation 'com.google.android.gms:play-services-auth:19.0.0'
	implementation 'com.google.http-client:google-http-client-gson:1.26.0'
	implementation('com.google.api-client:google-api-client-android:1.26.0') {
		exclude group: 'org.apache.httpcomponents'
	}
	implementation('com.google.apis:google-api-services-drive:v3-rev173-1.25.0') {
		exclude group: 'org.apache.httpcomponents'
	}
	...
}

ログイン

ログイン用にGoogleSinginClient変数、Google Driveを操作するためにDrive変数を定義します。

private var googleSigninClient: GoogleSigninClient? = null
private var drive: Drive? = null

Googleへのログイン処理は以下の通りです。

private val driveContent =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK && it.data != null) {
                // ログイン成功
		connectDrive(it.data!!)
            } else {
                // ログイン失敗orキャンセル
            }
        }

private fun loginToGoogle() {
	val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestScopes(Scope(DriveScopes.DRIVE_FILE))
                .requestEmail()
                .build()
	googleSignInClient = GoogleSignIn.getClient(activity, googleSignInOptions)
        val intent = googleSignInClient.signInIntent
	driveContent.launch(intent)
}

ログイン処理が終わると、registerForActivityResultに返って来るので処理します。ログイン成功後、Google Driveにアクセスするための準備を行います。

private fun connectDrive(intent: Intent) {
	GoogleSignIn.getSignedInAccountFromIntent(intent)
		.addOnSuccessListener {
			val credential = GoogleAccountCredential.usingOAuth2(
			    context, Collections.singleton(DriveScopes.DRIVE_FILE)
			)
			credential.selectedAccount = it.account
			drive = Drive.Builder(
			    AndroidHttp.newCompatibleTransport(),
			    GsonFactory(),
			    credential
			).setApplicationName(getString(R.string.app_name)).build()
		}
		.addOnFailureListener {
			Log.w("Sign in failed", it.toString())
		}
}

Google Driveへの処理

ファイルIDの生成

新しくGoogle Driveにファイルを作成する場合、ファイルIDを生成する必要があります。

fun createDriveIdTask(
        parents: List<String>,
        mimeType: String,
        fileName: String
    ): Task<String> {
        val taskCompletionSource = TaskCompletionSource<String>()
        val metadata = File()
                .setParents(parents)
                .setMimeType(mimeType)
                .setName(fileName)
        val file =
                drive.files().create(metadata).execute() ?: throw IOException("Result is null")
	taskCompletionSource.setResult(file.id)
        return taskCompletionSource.task
}

TaskCompletionSourceTaskベースのAPIを作成する機能を提供してくれるクラスです。非同期処理から返されたTaskに結果や例外を設定して使用します。

ファイルの保存・更新

ファイルIDを指定して、保存および更新を行います。

fun saveFile(
        fileId: String,
        fileName: String,
        contentType: String,
        data: ByteArray
    ): Task<Unit> {
        val taskCompletionSource = TaskCompletionSource<Unit>()
        val metadata = File().setName(fileName)
        val contentStream = ByteArrayContent(contentType, data)
	drive.files().update(fileId, metadata, contentStream).execute()
        taskCompletionSource.setResult(null)
        return taskCompletionSource.task 
}

これらを実際に使ってGoogle Driveにファイルをバックアップする例が以下の関数です。

fun backupToDrive(fileName: String, mimeType: String, fileContent: ByteArray) {
        val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
        coroutineScope.launch {
            createDriveIdTask(
                listOf("root"),
                mimeType,
                fileName
            ).addOnSuccessListener {
		// ファイルIDの生成成功
                val fileId = it
                saveFile(
                    fileId,
                    fileName,
                    mimeType,
                    fileContent
                ).addOnSuccessListener {
                    // バックアップ成功
		    Log.d("Success", "Backup succeeded")
                }.addOnFailureListener { exception ->
                    // バックアップ失敗
                    Log.w("Failure", exception.toString())
                }
            }.addOnFailureListener { exception ->
                // ファイルIDの生成失敗
                Log.w("Failure", exception.toString())
            }
        }
}

Discussion

androidアプリの開発未経験で、現在勉強中のものです。
スマホから写真をファイル名を変更してGoogle Driveにアップロードするアプリの作成に挑戦しています。
ここでの記事がとても参考になり、手順に従って操作していたのですが「SHA-1 証明書フィンガープリント」で躓いてしまいました。コマンドプロンプトでは以下のとおりエラーが出てしまいます。
もしよろしければ、対策方法をご教授いただけませんか?

C:\pg\java\jdk-17.0.2\bin>keytool -list -keystore YOUR_KEYSTORE_PATH -v
keytoolエラー: java.lang.Exception: キーストア・ファイルは存在しません: YOUR_KEYSTORE_PATH
java.lang.Exception: キーストア・ファイルは存在しません: YOUR_KEYSTORE_PATH
at java.base/sun.security.tools.keytool.Main.doCommands(Main.java:915)
at java.base/sun.security.tools.keytool.Main.run(Main.java:415)
at java.base/sun.security.tools.keytool.Main.main(Main.java:408)

アプリへ署名する際にキーストアを作成します。そのとき、キーストアを保存するパスを設定するので、keytool -list -keystore YOUR_KEYSTORE_PATH -vYOUR_KEYSTORE_PATHをそのパスを置き換えて入力後、キーストア作成時に設定したパスワードを入力すれば、証明書のフィンガープリントがターミナルに表示されます。
キーストアの生成については公式ドキュメントに載っているのでそちらを参照してください。

https://developer.android.com/studio/publish/app-signing?hl=ja#generate-key

早々のご回答、そしてこんな初歩的な質問に丁寧にご説明頂きありがとうございました。
スマホアプリを作成し始めるまでの環境設定が複雑で、まだAndroid StudioでHello World!すらかけていません。
長い道のりになりそうです。

アプリ開発は大変な点も多いですが面白いところもたくさんあるので、アプリ完成に向けて頑張ってください!応援しています!

私、初心者でAmeganeさんの記事を参考にさせていただき上記のプログラムを作成していたのですが
googleSignInClient=GoogleSignIn.getClient(activity,googleSignInOptions)
のactivityの部分と
val credential = GoogleAccountCredential.usingOAuth2(
context, Collections.singleton(DriveScopes.DRIVE_FILE)
)
のcontextの部分において
Unresolved reference: というエラーが出てしまいました。

検索しても自分自身が無知なため解決できずにいます。
もしよろしければエラーの解決法をご教授いただけませんか?

Unresolved referenceは定義されていない変数や関数などを参照した際に発生するエラーです。今回はcontextがどこにも定義されていないので、ビルドエラーとなっています(記事内のコードでも定義していないです)。Contextの詳しい話は紹介している記事がたくさんあるので割愛しますが、Contextの取得方法はActivity内の場合はthis、Fragment内の場合はrequireContext()、Jetpack ComposeによるComposableな関数内の場合はLocalContext.currentです。

// Activity
val credential = GoogleAccountCredential.usingOAuth2(this, Collections.singleton(DriveScopes.DRIVE_FILE))

// Fragment
val credential = GoogleAccountCredential.usingOAuth2(requireContext(), Collections.singleton(DriveScopes.DRIVE_FILE))

// Jetpack Compose
val context = LocalContext.current
val credential = GoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE_FILE))

大変参考になりました。
ところで、On**Listenner の内部処理は MainThread で走ると思うのですが、上記コードのsaveFile の走るスレッドはMainThread という設計意図なのでしょうか?

記事中のコードはあくまでもサンプルなのでスレッドには言及していません。実際のアプリで使う場合はIOスレッドで行います。

ログインするとコメントできます