💾

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

2021/05/03に公開
46

追記(2022/09/26)

この記事に書かれているコードは記事作成時点のものです。そのため、最新のライブラリのバージョンだと動かない可能性があります。
また、多くの人に閲覧していただいて大変ありがたいのですが、一方でコメント欄にて本記事と直接関係のない質問が寄せられるようになってきました。今後はこの記事にて言及していない箇所の質問に対して返信しないのでご了承ください。


この記事では、デベロッパーが作成した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

hamazohamazo

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)

AmeganeAmegane

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

hamazohamazo

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

AmeganeAmegane

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

mm_Tmm_T

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

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

AmeganeAmegane

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))
facepolishfacepolish

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

AmeganeAmegane

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

だにぱうだにぱう

Androidアプリを作成しています。アプリで作成したデータはSqliteに保存するようにしています。
今回このSqliteのデータに対して、アプリからバックアップと復元をする機能を作りたいと思っているのですが、この記事に書かれているやり方をベースにして実現可能でしょうか?もしよろしければ教えて下さい。

AmeganeAmegane

可能です。SQLiteで保存しているDBからデータを取得してそれをByteArrayに変換すれば良いと思います。

だにぱうだにぱう

早速ありがとうございます。
チャレンジしてみたいと思います。

だにぱうだにぱう

if (it.resultCode == Activity.RESULT_OK && it.data != null) {

早速試してみましたが、it.resultCodeが常に0を返しえており、うまくいきません。
色々調べてましたが、解決せず・・・このような症状はご経験ないでしょうか?

AmeganeAmegane

その情報だけだと何とも言えないですが、考えられるのはログインに失敗してることですかね…
ログイン用の画面は出てきてますか?

だにぱうだにぱう

SHA1の登録がまずかったようです。
C:\Users[ユーザー].android\debug.keystore のSHA1を使用したらとりあえずログインできました。作成したキーストアでやるとうまくいきませんでした。原因わかりますでしょうか・・・

だにぱうだにぱう

度々申し訳ありません。
Amegane様のコードを参考にして何とかUploadが出来たようです。(ちなみに、私はJavaで書いています)本当にUploadできたのかを確認したいのですが、確認方法をご教示頂けないでしょうか。

AmeganeAmegane

C:\Users[ユーザー].android\debug.keystore のSHA1を使用したらとりあえずログインできました。作成したキーストアでやるとうまくいきませんでした。原因わかりますでしょうか・・・

アプリの署名に使われたSHA-1証明書フィンガープリントを登録する必要があります。証明書はデバッグビルドやリリースビルド時にAndroid SDK Toolsによって自動生成されるので基本的には自分で生成する必要はないです。

本当にUploadできたのかを確認したい

ログインしたアカウントのGoogle Driveを見るか、ログインしたアカウントのGoogle Driveにあるデータを取得する処理を書くと確認できます。

だにぱうだにぱう

たくさん質問してしまい申し訳ありません^^;本当に感謝しかありません。

アプリの署名に使われたSHA-1証明書フィンガープリントを
登録する必要があります。
はい、はじめはこちらの様に署名時に生成されるSHA1を登録しておりましたが、それだとresultCodeが常に0になりました。
https://developer.android.com/studio/publish/app-signing?hl=ja#generate-key

次にこちらの方法で登録したところうまく認証できました。
https://youtu.be/XAb5rR54gdc

後者の方法で大丈夫なのでしょうか。ちなみにAPIサービスをまだ公開しておらずテストでモードで試している状況です。

だにぱうだにぱう

ログインしたアカウントのGoogle Driveを見る

確認できました。アップロードは成功しているようです。ありがとうございました。

Google Driveにあるデータを取得する処理を書く

アップロードは出来たので、次にdbファイルをダウンロードしてリストアしたいと思っています。他サイトでも調べたりしているのですが、情報が古すぎたり推奨されていないやり方でどれも参考になりせんでした。大変おこがましいのですが、もし可能でしたらDriveからダウンロードするやり方をご教示頂けないでしょうか。

AmeganeAmegane

SHA-1フィンガープリント証明書の取得は動画の方法でも大丈夫です。

もし可能でしたらDriveからダウンロードするやり方

ファイルを取得する関数があるのでこれを使って取得できます。

だにぱうだにぱう

よかったです。安心しました。

関数のサイトすっごい助かります。これで出来そうです。

長々と質問してしまい大変申し訳ありませんでした。
今後とも宜しくお願い致します。

だにぱうだにぱう

SHA-1フィンガープリント証明書の取得は動画の方法でも大丈夫です。

開発(デバッグ)時はアップロード出来ていますが、Playストアにあげたアプリで試すと失敗します(ログイン画面は出る)。。

何かわかりますでしょうか?何回も申し訳ありません。。

AmeganeAmegane

開発(デバッグ)時はアップロード出来ていますが、Playストアにあげたアプリで試すと失敗します(ログイン画面は出る)。。

OAtuthクライアントIDに設定しているフィンガープリントはリリース用の証明書のフィンガープリントになってます?

だにぱうだにぱう

はい、なっています。

keytool -list -keystore YOUR_KEYSTORE_PATH -v

こちらで確認したSHA1をGoogleCloudにSHA1に設定しています。。

AmeganeAmegane

では、Google Cloud ConsoleのOAuth同意画面の公開ステータスは本番環境に変更しましたか?

だにぱうだにぱう

一応画像送りますね。この辺の設定がおかしいとおもいますので。。

アプリは審査されていないため、

というのが気になりました。

だにぱうだにぱう

でもたぶん、スコープはauth/drive.appdataとauth/drive.Fileだけ選択してあるので審査は必要なないですよ、って事を言ってるんですよね。。ここでハマるとは思っていませんでした(悲)

AmeganeAmegane

スコープ的に審査は必要ないはずです。画像にある特定のOAuthスコープは機密性の高いスコープや制限付きスコープを指すので。
となるとちょっと分からないですね…こちらでも調べてみますが返信に時間がかかるかもしれないです。

だにぱうだにぱう

となるとちょっと分からないですね…こちらでも調べてみますが返信に時間がかかるかもしれないです。

コメント見落とされてるようですね^^
上のstackoverflowをみて解決しました!

AmeganeAmegane

あ、すみません、しばらくページ開きっぱなしにした状態のまま返信したのでコメント見落としてました…
自己解決されたようで何よりです!

だにぱうだにぱう

前回のお礼を忘れておりました。
おかげさまでアップロード、ダウンロード共にデバッグ環境では成功しました!ありがとうございました!

barbatosbarbatos

お初にお目にかかります。
Androidアプリ開発未経験で現在勉強中の者です。
Amegane様の記事を拝見し,「Google Cloud PlatformでGoogle Drive APIを有効化」「OAuth同意画面の作成」「認証情報の作成」まで進めることができ,大変参考になりました。ありがとうございます。

質問したいことがございます。
下記のサンプルコードを基に,撮影した写真をGoogle Driveの任意のフォルダに保存する機能を実装したいと考えております。
https://github.com/android/camera-samples/tree/main/CameraXBasic
しかし,色々調べたところ,カメラアプリの保存先をGoogle Driveに変更するといった機能を実現させる上で,参考になりそうなサイトが見当たらずどうしたものかと思案しています。
そこで質問二点です。
1.上記の機能を実現するために,まずどんなことを学ぶとよいでしょうか。
2.上記の機能を実現するために参考になりそうなサイトはありますでしょうか。
酷く抽象的な質問となってしまい,大変申し訳ございません。
回答よろしくお願いいたします。

AmeganeAmegane

上記の機能を実現するために,まずどんなことを学ぶとよいでしょうか。

CameraX周りの知識だと思います。

上記の機能を実現するために参考になりそうなサイトはありますでしょうか。

公式のドキュメントやリファレンスが参考になると思います。

https://developer.android.com/training/camerax/take-photo?hl=ja
https://developer.android.com/reference/androidx/camera/core/package-summary?hl=ja
https://developer.android.com/reference/androidx/camera/core/ImageCapture?hl=ja

barbatosbarbatos

再度の質問すみません,
cameraXの勉強をしていて,気が付いたのですが,本ページの関連ライブラリを追記すると”Unable to resolve dependency for~(以下画像の通りです。)”というエラーが出ます。
また,その状態で本ページのログインの際の
private var googleSigninClient: GoogleSigninClient? = null
private var drive: Drive? = null
をプログラムの中に書くとDriveに関してUnresolved referenceとエラーが出ます。

APIが有効化できていないもしくは,その旨をアプリに記述できていないのではと思ったのですが,何が原因でしょうか。また,原因を探るためにAmegane様に提供すべき情報で足りないものなどございましたら,お手数おかけしますがご教授いただけると幸いです。

AmeganeAmegane

WarningにDisable offline mode and sync projectと出ているので、GradleをSyncするときoffline modeになっていると考えられます。
なので、offline modeを解除すればSyncできると思います。

barbatosbarbatos

返信遅くなり申し訳ありません。
無事offline modeを解除してSyncできました。
アドバイスいただきありがとうございました。

barbatosbarbatos

回答ありがとうございました。
ご紹介いただいた公式のドキュメントやリファレンスを参考にしながら,cameraXに関する勉強を始めようと思います。

barbatosbarbatos

以前質問させていただいた者です。
とても初歩的な質問を繰り返してしまい,すみませんでした。
親切に回答いただけたおかげで,開発を続けることができており,大変感謝しております。
再び躓いてしまった箇所があり,二点質問させてください。

一点目です。
Googleへのログイン処理に関してなのですが,下記コードのit.resultCodeから0が返されるときと,-1が返される時があります。よってログインが成功するときと失敗するときがあり,原因がわからない状態です。この場合原因として,どんなことが考えられますでしょうか。

if (it.resultCode == Activity.RESULT_OK && it.data != null)

二点目です。
本記事内で下記コードがあり,いずれも変数driveを使用していますが,android studioで下記コードを書くと,変数driveがnullのため,Replace with safe (?.) callというエラーが出ます。
ファイルIDの生成

val file = drive.files().create(metadata).execute() ?: throw IOException("Result is null")
ファイルの保存・更新
drive.files().update(fileId, metadata, contentStream).execute()

エラーからの提案に従いセーフコール演算子を反映させると下記コードのようになりエラーは消えるのですが,問題ないでしょうか。また,この場合変数driveにはログイン後に必要な情報が入るという認識で間違いないでしょうか。
ファイルIDの生成
val file = drive?.files()?.create(metadata)?.execute() ?: throw IOException("Result is null")
ファイルの保存・更新
drive?.files()?.update(fileId, metadata, contentStream)?.execute()

一度に二つも質問をしてしまい,申し訳ありません。
もしよろしければご教授いただけますと幸いです。

AmeganeAmegane

it.resultCodeから0が返されるときと,-1が返される時があります

アプリやユーザ側で何か処理を行ったり画面を操作したりしているのでなければちょっと原因はわからないですね…。

エラーからの提案に従いセーフコール演算子を反映させると下記コードのようになりエラーは消えるのですが,問題ないでしょうか

問題ないです。
ただ、保存・更新のほうはnullだったときに例外をスローするといった処理を別途書く必要があります。自身のアプリにあった処理を書いてください。
また、記事内のコードはサンプルなので、non-nullにしたりシングルトンにしたりするなどアプリの設計に応じて変えてみることをおすすめします。

barbatosbarbatos

返信ありがとうございます。
一点目に関してです。

アプリやユーザ側で何か処理を行ったり画面を操作したりしているのでなければ

上記に関して考えたのですが,Googleへのログイン処理をしている裏で,アプリかユーザが何かしら動作をすると問題があるということでしょうか。

二点目に関してです。
アドバイスありがとうございます。どんな時に例外をスローする処理,non-null,シングルトンを用いるべきか調べて,アプリにあったものは何か考えます。

AmeganeAmegane

上記に関して考えたのですが,Googleへのログイン処理をしている裏で,アプリかユーザが何かしら動作をすると問題があるということでしょうか

すみません、言葉不足でした。その解釈で合ってます。
例えばログイン処理を実施している際に画面が暗くなりますが、そのとき画面をタップするとログイン処理が中断されます。

あとは偶にログインが成功しているので可能性は低いと思いますが、念のためSHA-1証明書やOAuthクライアントIDの設定が正しいか確認するのもありかもしれません。

barbatosbarbatos

例えばログイン処理を実施している際に画面が暗くなりますが、そのとき画面をタップするとログイン処理が中断されます。

わかりやすい具体例をありがとうございます。とても助かります。
先ほどSHA-1証明書を確認したところ,PCを変えたからか設定が異なっていました(なぜこの状況でログイン成功したことがあったのかよくわかりませんが...)。まずはその設定を修正するところから始めようと思います。

barbatosbarbatos

いつもお世話になっております。
ログイン処理ならびにGoogle Driveへのファイルのアップロードができましたのでご報告です。今回私はカメラアプリを作成し,撮影した画像をGoogle Driveにアップロードするという動作を実現するためにAmegane様のこのサイトをたくさん参考にさせていただきました。心から感謝申し上げます。ありがとうございました。