なるようになるかも

力は多くの場合、その人の思いを超えない。

Google Drive Android API (2014/02) のメモ書き

GoogleApiClientをどう扱えばいいのか微妙に悩みどころ。

ドキュメントには書いていないAPIリミットがあるケースもありえるので、頻繁に接続/切断してしまっても構わないのか、それともある程度大きなスコープで保持しておくべきなのか。

指針となるのが、

You should instantiate this object in your Activity's onCreate(Bundle) method and then call connect() in onStart() and disconnect() in onStop(), regardless of the state.

この記述なんだけど、Googleの提供するサンプルソースはこれに従ってないんだよね…。

認証周り

たぶん一番躓くのが、Eclipseの場合にgoogle-play-services_libのプロジェクトを一度インポートしないといけないことだと思うけど、その辺は他のサービス(プッシュ通知とか)も共通なので割愛。

Google API Clientの生成

 mGoogleApiClient = new GoogleApiClient.Builder(this)
   .addApi(Drive.API)
   .addScope(Drive.SCOPE_FILE)
   .addConnectionCallbacks(this)
   .addOnConnectionFailedListener(this)
   .build();

ビルダーに渡すcontextActivityでもApplicationでもいいらしい。

Google Drive APIを使う場合、addApi()com.google.android.gms.drive.Drive.APIを渡す。

addScope()は情報取得のパーミッションメタデータだけを取得、アプリケーションデータの読み書き、データの読み込みのみ等)を設定するのだけど、現在のところはDrive.SCOPE_FILE(アプリ自身が生成したファイルの読み書き)に限定されている。

より高度なことがやりたい場合、Java向けのライブラリが使う必要がある。

あとは成功時と失敗時のコールバックを受け取るインスタンスを渡して、build()してconnect()を呼ぶだけ。

失敗時の処理

何らかの理由で失敗した場合はコールバックメソッドが呼ばれる。ちなみに、APIコンソールにkeystoreのSHA1ハッシュとpackage名を登録していない場合は、認証エラーじゃなくてINTERNAL_ERRORになる。

final private static int RESOLVE_CONNECTION_REQUEST_CODE = 1;

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
  //解決策があるかどうか
  if (!connectionResult.hasResolution()) {
    //ないので諦める(自動でエラーメッセージを表示してくれる)
    GooglePlayServicesUtil.getErrorDialog(connectionResult.getErrorCode(), this, 0).show();
    return;
  }

  try {
      //GooglePlayServicesのActivityに解決を委譲する
      connectionResult.startResolutionForResult(this, RESOLVE_CONNECTION_REQUEST_CODE);
  } catch (IntentSender.SendIntentException e) {
    // この例外がきたらどうしようもない。諦めのメッセージを出そう
    }
  }
}

RESOLVE_CONNECTION_REQUEST_CODEstartActivityForResult()の第二引数と同じ意味。識別のために、適当な定数を定義しておく必要がある。

@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
  if (requestCode == REQUEST_CODE_RESOLUTION && resultCode == RESULT_OK)) {
    //次のconnect()は上手くやってくれるでしょう
    mGoogleApiClient.connect();
  }
}

もしGoogle Play ServicesのActivity側で解決された場合、先ほど定義した定数でそれを識別し、resultCodeRESULT_OKであれば再度connect()を呼び出す。

成功時の処理

接続に成功した場合、onConnected()が呼び出される。

@Override
public void onConnected(Bundle connectionHint) {
  // 接続成功
}

connectionHintにはサービス毎に定義される何らかの情報が入っている。nullの場合は、何も情報がないことを意味していて、Drive APIでも現時点ではnullが返される。

フォルダとデータの扱い

Google Driveに保存されたファイルは抽象クラスであるDriveFile、ディレクトリはDriveFolderとして扱う。

なぜこれらのインターフェースを経由する必要があるかというと、アイテムの実体はGoogle Drive上にあり、実際のデータ取得を遅延させる/ローカルにキャッシュするため。

アイテム(フォルダおよびファイル)は一意なDriveIdインスタンスで識別される。DriveIdString型にシリアライズ/デシリアライズすることができるので、HashMapなどに一時的に保存することができる。

この辺、リファレンスを見るだけだと良く分からないのだけど、encodeToString()で取得できる文字列は、ローカルキャッシュを識別する値を返して、getResourceId()を使うとリモートの実体を表す値を取れる(nullが返却されることもある)。

decodeFromString()ではどちらも利用できるのかな?

ファイル/フォルダ生成はGoogle Play ServicesのActivityを経由して行う

アイテムの新規生成時にもGoogle Play ServiceのActivityに処理を委譲するため、次のようなダイアログが表示される。

f:id:quesera2:20140301235955p:plain

このため、新規にアイテムを生成する場合にはActivityContextが必須となる。

ちなみに今のバージョンだと、上のようにアイテム名を日本語のまま作成すると文字化けしてしまう。Drive APIの注目度の高さが伺えますね…。

updateMetadata()でアイテム名を書き換える場合には、日本語でも大丈夫みたいなので、Google Play ServiceのActivity側のバグなんでしょうか。

データの作成/変更は非同期で処理を行う

Drive.DriveApiに対して特定のDriveFileの取得や、新規作成を行うのだけど、当然データはクラウド上にあるので、データに対する処理は全て非同期で行う。このための共通コールバック用クラスとして、ResultCallbackが用意されている。(余談だけど、一つ前のバージョンでは新規アイテム作成時のコールバッククラスは別だった)

例えばHello world.と書かれたテキストドキュメントを生成するのはこんな感じ。

final private static int REQUEST_CODE_CREATOR = 2;

private void createNewFile(){
  // ファイルの新規作成のクエリーを実行
  Drive.DriveApi.newContents(mGoogleApiClient).setResultCallback(new ResultCallback<ContentsResult>(){
    @Override
    public void onResult(ContentsResult result) {
      // 通信障害などで失敗した場合はその通知を行う
      if (!result.getStatus().isSuccess()) {
        return;
      }

      // 新規ファイル生成の場合、getContents().getOutputStream()で
      // ファイルのコンテンツのストリームを取得できる
      OutputStream outputStream = result.getContents().getOutputStream();
      try {
        outputStream.write("Hello world".getBytes());
      } catch (IOException e) {
        // この時点ではローカルへの書き込みなのでまず失敗することはないと思うけど、
        // IO例外の場合は処理を止めるなりする
        return;
      }

      // ファイルのメタデータは、MetadataChangeSetBuilderで生成
      MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
      .setMimeType("text/plain").setTitle("new file").build();

      // コンテンツとメタデータからファイル生成Intentを取得
      IntentSender intentSender = Drive.DriveApi.newCreateFileActivityBuilder()
      .setInitialMetadata(metadataChangeSet)
      .setInitialContents(result.getContents())
      .build(mGoogleApiClient);
      try {
         // ファイル生成確認のダイアログ風のActivityが表示される
         // 成功すれば、新規ファイル作成がcommitされる
        startIntentSenderForResult(intentSender, REQUEST_CODE_CREATOR, null, 0, 0, 0);
      } catch (SendIntentException e) {
        // GooglePlayServicesのActivityを呼び出せなかった場合はその旨を通知
       }
    }
  });
}

DriveFileを指定せずnewContents()を呼び出した場合、新規ファイルはルートディレクトリに生成される。処理を時系列にするためにこういう書き方にしているけれど、実際にはこんなコールバック地獄な書き方はしない方が良い。

newContents()メソッドなどの返却型はPendingResultで、await()メソッドを用いることで同期処理にすることもできる。

private void createNewFile(){
  // ファイルの新規作成のクエリーを実行
  ContentsResult result = Drive.DriveApi.newContents(mGoogleApiClient).await();
  // 本当はawait()にタイムアウトの秒数を渡す必要がある
  // タイムアウト時にはgetStatusCode()としてINTERRUPTEDが返却される
  if (!result.getStatus().isSuccess()) {
    return;
  }
  //以下省略...
}

当然だけど、UIスレッドをブロックしてはいけない。この場合、AsyncTaskLoaderFragmentでワーカークラスを実装するのがよさそう。

あと厄介そうなのはファイルの読み書き両方を行いたいときにはParcelFileDescriptorを使う必要があることくらいかな?

要領が分かればあとはなんとかなりそう。