【Android】Google DriveのApp Folderにデータを保存する

***残念ながらDrive APIは[Deprecated](https://developers.google.com/drive/android/deprecation)となりました。2019/12/6に完全廃止となるようです。今後は、[Drive REST API](https://developers.google.com/drive/api/v3/about-sdk)に移行するか、Firebaseなどクラウドストレージの利用を検討した方がいいでしょう。***

# App Folderとは
Google Driveの[App Folder](https://developers.google.com/drive/android/appfolder)は、他のアプリやユーザ自身でもデータの内容を閲覧・編集することができない**アプリ専用フォルダ**です。そのため、アプリの設定値やゲームのセーブデータなどを保存するのにも使えそうです。ただし、ユーザのDrive容量を消費するため、大容量データを保存するのは控えた方が良いでしょう。

App Folderの操作には、[Google Drive API]((https://developers.google.com/drive/android/get-started))を使用します。この記事では、App Folderにデータを保存する実装例を紹介します。
Google Play Services SDKを利用しますので、まだダウンロードしていない場合は[こちら](https://developers.google.com/android/guides/setup)を参照してDLしてください。

# Google API Consoleでプロジェクトの作成
1. [Google API Console](https://console.developers.google.com/projectcreate)で新しいプロジェクトを作成します
1. API Manager > 左パネルのライブラリから「Google Drive API」を有効化します
 
※ App Folderにはプロジェクト単位でデータが保存されるようなので、**一つのアプリにつき一つのプロジェクトを作成**した方が良いです。

# Google API Consoleで認証情報を登録
次に[Google API Console](https://console.developers.google.com/apis/credentials)で認証情報を登録します。詳しくは[こちら](https://developers.google.com/drive/android/auth)を読んでください。

<手順>
1. 作成したプロジェクトにて、「認証情報を作成」をクリック
2. **OAuthクライアントID**を選択
3. **Android**を選択
4. 名前は適当な名前を入力 (my-android-app など)
5. Releaseビルド時の署名証明書のフィンガープリント(SHA1)を入力 (後述)
6. アプリのパッケージ名を入力
7. 「作成」をクリック

認証情報の登録にはSHA1が必要になりますが、注意しないといけないのは**Debugビルド用とReleaseビルド用でSHA1が異なります**。

Debugビルド用のSHA1は以下のコマンドで取得します。

```
$ keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v
```
パスワードは **android** です。

Releaseビルド用のSHA1も同様に取得します。

```
$ keytool -exportcert -keystore <YOUR_KEYSTORE_PATH> -list -v
```

アプリに署名するエイリアスのSHA1をコピペしてください。
なお、**Releaseビルド用のSHA1は別プロジェクトで登録済みだと、重複して登録できなかった**ので、アプリごとにKeystoreのエイリアスを分けるなど対策の必要がありそうです。

また、**同じパッケージ名で複数認証情報を登録すると不具合が生じるようです**。
実際に、Releaseビルド用とDebugビルド用で同じパッケージ名で登録していたら、Releaseビルドではファイルが見つからないといったバグに悩まされました。。。(公式ドキュメントを読んでもよく分からず)
なので、僕はDebugビルドはパッケージ名を別名にして対策しています。

# Androidアプリの作成
ここからはAndroidアプリの実装に入ります。Console上の設定は複雑でしたが、実装自体は比較的簡単です。

## パッケージ名
上記の認証情報の作成時に入力したパッケージ名と同じにします。

## build.gradleの設定
appのbuild.gradleにplay-servicesを追加します。

```groovy:build.gradle
dependencies {
    compile 'com.google.android.gms:play-services:11.0.2'
}
```

## 実装(Java)
コード例を以下に貼ります。説明はコメントを参照していただければと思います。
この例はGoogle Driveにファイルを新規生成する例です。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity
    implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {

    private static final String TAG = "DriveSample";

　　 private static final int REQUEST_CODE_RESOLUTION = 1;

    private GoogleApiClient googleApiClient = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        googleApiClient = new GoogleApiClient.Builder(this)
            .addApi(Drive.API)
            .addScope(Drive.SCOPE_FILE)
            .addScope(Drive.SCOPE_APPFOLDER)　　// AppFolderの利用
            .addConnectionCallbacks(this)
            .addOnConnectionFailedListener(this)
            .build();

        // 接続ボタンの実装
        Button connectButton = (Button) findViewById(R.id.connectButton);
        connectButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (googleApiClient != null && !googleApiClient.isConnected()) {
                    // 接続開始
                    googleApiClient.connect();
                }
            }
        });

        // 接続解除ボタンの実装
        Button disconnectButton = (Button) findViewById(R.id.disconnectButton);
        disconnectButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (googleApiClient != null && googleApiClient.isConnected()) {
                    // 接続解除
                    googleApiClient.disconnect();
                }
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == REQUEST_CODE_RESOLUTION && resultCode == RESULT_OK) {
　　　　　　　　　　　　　　　　　　　　　　　　// アクセス承認を得たので再接続開始
            googleApiClient.connect();
        }
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        // 接続完了したのでDriveに新しいコンテンツを生成する
        Drive.DriveApi.newDriveContents(googleApiClient)
            .setResultCallback(driveContentsResultCallback);
    }

    @Override
    public void onConnectionSuspended(int i) {
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult result) {
        if (!result.hasResolution()) {
            // show the localized error dialog.
            GoogleApiAvailability.getInstance().getErrorDialog(this, result.getErrorCode(), 0).show();
            return;
        }

        try {
            // ユーザにGoogle Driveへのアクセス承認ダイアログを表示する
            // onActivityResultに結果が通知されるので第二引数に指定したcodeで判別する
            result.startResolutionForResult(this, REQUEST_CODE_RESOLUTION);
        }
        catch (IntentSender.SendIntentException e) {
            // Exception while starting resolution activity
        }
    }

    private final ResultCallback<DriveContentsResult> driveContentsResultCallback = new ResultCallback<DriveContentsResult>() {
        @Override
        public void onResult(@NonNull DriveContentsResult result) {
            if (!result.getStatus().isSuccess()) {
                // Failed to create new content
                return;
            }

            final DriveContents driveContents = result.getDriveContents();

            OutputStream outputStream = driveContents.getOutputStream();
            Writer writer = new OutputStreamWriter(outputStream);

            try {
                // 適当なJSONデータを書き込む
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("key1", true)
                    .put("key2", 100.0)
                    .put("key3", 123);
                writer.write(jsonObject.toString());
                writer.close();
            }
            catch (JSONException | IOException e) {

            }

            // "sample.json"というファイル名でファイル保存する
            MetadataChangeSet changeSet = new MetadataChangeSet.Builder()
                .setTitle("sample.json")
                .setMimeType("text/plain")
                .build();

            Drive.DriveApi.getAppFolder(googleApiClient)
                .createFile(googleApiClient, changeSet, driveContents)
                .setResultCallback(fileCallback);
        }
    };

    private final ResultCallback<DriveFileResult> fileCallback = new ResultCallback<DriveFileResult>() {
        @Override
        public void onResult(@NonNull DriveFileResult result) {
            if (!result.getStatus().isSuccess()) {
                // Failed to create a file
                return;
            }

            // ファイルの作成に成功するとDriveIdが発行される
            // このDriveIdを保存しておくと、次回以降DriveIdを使ってファイル検索できる
            Log.d(TAG, "Success to create a file. " + result.getDriveFile().getDriveId());
        }
    };
}
```

この他にもファイルの検索・取得・削除といった操作が可能ですので、公式ドキュメントを参照してみてください。

# まとめ
App Folderの使い方を紹介しました。公式ドキュメントにわかりづらい部分もありますが、アプリデータのバックアップなどにも利用できるので、ぜひ使ってみてください。