2016/07/17

Android Backup Service

詳しい話はここに書いてあるけど英語だし。。。
なので、実装した上での経験も含めてまとめた。


前置き

Android Backup ServiceはAndroid2.2から実装された、Androidアプリのデータバックアップの仕組み。
BackupManagerというクラスを通して、バックアップを行う。
主な特徴は下記の通り。

  1. バックアップ内容は、保存したい内容をbyte列にしてwriteする方法と、ファイルやSharedPreferencesを指定する方法がある。
  2. ファイルに関しては、基本的にはContext.getFilesDir()のパス内に保存されているデータを指定できる。(それ以外にもできるという記事がよそにありますが)
  3. バックアップのタイミングは指定できない。(OSが定期的にバックアップを行う)
    できるのは対象データが変更されたのを通知することのみ。
  4. リストアもタイミングを指定できないとあるが、実際に実装してみたらアプリ側でリクエストしたらすぐに処理が始まるっぽい。
  5. 画像やテキストデータでもバックアップ可能。データ容量制限は記載がない。
  6. 最終的にバックアップを行うかどうかはユーザがAndroid端末の設定アプリから設定できる。

3のため、ゲームのセーブデータ共有などには向かない。(セーブしたタイミングでバックアップが走るとは限らないので)
4は実際に試したところ、そんな感じの挙動だった。



実装手順


実装するおおまかな手順は

  1. Android Backup ServiceにPackage nameを登録し、API Keyを取得する。
  2. BackupAgent、もしくはBackupAgentHelperを拡張した自前のBackupAgentを作成し、バックアップ/リストア時の挙動を実装する。
  3. AndroidManifest.xmlにAPI KeyとBackupAgent名を設定する。
  4. バックアップ/リストア要求を実装、テストする。

といった感じ。
4のテストなんかわざわざ書かなくても...と思うかもだけど、実はここに結構罠がある。



1.Android Backup Serviceへの登録


ここにアクセスしてPackage Name(com.example.appnameみたいなやつ)を登録する。
登録後にKeyが発行・表示される。AndroidManifest.xmlに登録するAPI Keyを含んだmeta-data要素も表示してくれるので、これをコピーしておく。



2.BackupAgentの実装


android.app.backup.BackupAgentHelperを拡張する方法と、android.app.backup.BackupAgentを拡張する方法がある。

BackupAgentHelperはファイルやSharedPreferencesをバックアップする方法で、OnCreate()で対象を指定するだけでバックアップ/リストアができる。
ただ、保存対象を動的に変更することが難しい。
いつも決まったファイルを保存するのなら、こっちで十分。

BackupAgentはバックアップ内容をbyte列にしてwriteする方法。byte列の内容は自分で管理する必要がある。
何かと自由度が高いが、実装コストが高い。


どちらの場合でも重要なメソッドは、バックアップ時の挙動を実装するonBackup()とリストア時の挙動を実装するonRestore()。
BackupAgentHelperはこれが既にいい感じに実装されている。
この2ハンドラはスレッドセーフではなく同時に処理が走ることがあるため、内部でロックをかけた方がいいとのこと。

BackupAgentHelperの拡張


こちらはファイルやSharedPreferenceの保存機能が既に実装されている。
onCreate()でファイルやSharedPreferenceを指定するだけでバックアップ/復元が可能。
・・・だけど、少しだけそれ以外の要素も考慮する必要がある。

保存対象の指定は下記の2つのクラスで行う。
  • SharedPreferencesBackupHelper
    バックアップ対象のSharedPreferencesを指定する。
  • FileBackupHelper
    バックアップ対象のファイルを指定する。

OnCreate()はこんな感じ

@Override
public void onCreate() {
    SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, "testprefs");  
    addHelper("prefs", helper);

    FileBackupHelper helper = new FileBackupHelper(this, "testfile.txt", "testimg.jpg"); //可変長引数で、ファイル名は複数指定可能
    addHelper("files", helper);
}

これだけでも、とりあえずバックアップと復元をやってくれる。
チョロい...と思いきや、上でちょっと説明したロックを考慮した方がいいみたいなので、onBackup()とonRestore()をちょっと拡張する。

@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
    Log.d("Backup", "Backup...");
    synchronized (lock) {
        try {
            super.onBackup(oldState, data, newState);
        }catch (Exception e){
            Log.d("Backup", "Backup error" + e.getMessage());
        }
    }
}

@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
    Log.d("Backup", "Restore...");
    synchronized (lock) {
        try {
            super.onRestore(data, appVersionCode, newState);
        }catch (Exception e){
            Log.d("Backup", "Restore error:" + e.getMessage());
        }
    }
}

lock変数はどこかで lock = new Object(); などしてね。

BackupAgentの拡張


こっちはコードを書くと結構ゴツいことになるので、手順だけ。
具体的なコードは公式に書いてあるので参考にしてね。

onBackup()では引数でoldState、data、newStateをとるが
  • oldState, newStateがバージョンやタイムスタンプなど、バックアップデータの状態を記載したファイルのインスタンス
  • dataは実際に保存されるデータのインスタンス
といったイメージ。(ちょっと語弊があるけど)
3つとも自分で内容を管理する必要がある。

まずは現在のデータの状態を確認する。

FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
DataInputStream in = new DataInputStream(instream);

といった形で、oldStateファイルを読んで状態を確認し、現在の状態と比較してバックアップするべきかを判定する。
あ、バックアップがない場合はolsStateはnullになるのでチェックを忘れずに。

バックアップするべきなら、dataのwriteEntityXXXXXという関数でバックアップデータを書き込む。
なんかHeaderとDataが書き込めるみたい。

バックアップデータを更新したら

FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
DataOutputStream out = new DataOutputStream(outstream);

といった形でnewStateファイルに新たな状態を書き込む。


onRestoreはdata、appVersionCode、newStateを引数としてとる。
  • dataは復元されるデータのインスタンス
  • appVersionCodeはバックアップが保存されたアプリのバージョン(AndroidManifest.xmlのandroid:versionCodeの値)。
  • newStateバックアップデータの状態を記載したファイルのインスタンス

といったイメージ(語弊(略))

onBackupで保存したnewStateなんかを渡してくれる。
dataからreadEntityXXXXXでデータを読み込んで反映する。


上記のようにバックアップするのはbyte列なので、その中のデータがどんな感じになっているかの情報を
newStateやdataのHeaderあたりに書くんでしょうね。


ロックに関しては、onBackupとonRestoreが同時に走っても問題ないよう、適宜synchronizedで囲う。



3.AndroidManifest.xmlの設定


修正は2点。

  • applicationタグ内にBuckupAgent名をしてする。
  • application要素内に、1で取得したmeta-data要素を記載する。

修正後がこんな感じ。

<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:backupAgent="MyBackupAgent"
    >
    ...
    <meta-data android:name="com.google.android.backup.api_key" android:value=YOURAPIKEY />
    ...
</application>

MyBackupAgentを2で実装したBackupAgent名に変更する。
YOURAPIKEYを1で取得したキーに入れ替る。



4.バックアップ/リストア要求の実装、およびテスト


実装・テストはアプリごとに異なるが、大まかな手順としては

  1. Android端末のバックアップ設定を行う。
  2. アプリをインストールする
  3. バックアップするデータを変更して、変更を通知してバックアップ要求を出す
  4. バックアップを行う
  5. アプリをアンインストールする
  6. アプリを再インストールする
  7. 復元要求を行う
  8. 復元処理終了後、対象データを確認する

バックアップ/リストア要求


3に関しては 、データを変更した後に

BackupManager bm = new BackupManager(this);
bm.dataChanged();

とする。
7も同様に、リストアしたいタイミングで

BackupManager bm = new BackupManager(this);
bm.requestRestore(restoreObserver);

とする。restoreObserverはリストア終了時などのハンドラを実装したクラスのインスタンス。
これで実装は終了。

端末設定


まずは端末のバックアップ設定を行う。設定アプリより「バックアップとリセット」を選択。
データのバックアップをONにする。

バックアップ/リストアテスト


バックアップはOSが定期的に行うので、しばらく待つ必要があるが
bmgrツールをコマンドから使用したら、強制的にバックアップ処理を走らせることができるとのこと。

・バックアップ要求
>adb shell bmgr backup [package]

・バックアップ関連の処理を走らせる
>adb shell bmgr run

・リストア要求
>adb shell bmgr restore [package]

・アプリのアンインストール(おまけ)
>adb uninstall [package]


いろいろな資料を見てたらbmgr runで処理を走らせてテストできるかな、と思ってたけど、
bmgr runでバックアップ処理が強制的に走るわけではなく、コマンドを打っても

前バックアップした時から時間が経ってないよ!

的なLogを出してバックアップされなかった。
バックアップもされてないので、リストアもされず、しばらく詰まった...


テストでは、何度か試すので、すぐにバックアップ処理が走らないと結構面倒。
そこでバックアップ場所の切り替えを行う。

>adb shell bmgr list transports

とコマンドすると、端末にあるバックアップ先がリストで表示される。

android/com.android.internal.backup.LocalTransport
* com.google.android.gms/.backup.BackupTransportService

手持ちの端末では、こんな感じに表示された。
これを

>adb shell bmgr transport android/com.android.internal.backup.LocalTransport

としてLocalTransportに切り替えると、即時バックアップが走った。
これで一連の動作を確認する。
(ただし、保存場所は端末のローカルになるので、後で戻す必要がある)


この辺りはデバイスによっても違うかもなので、参考程度に。


とりあえず、以上で実際に動くバックアップ機構を実装できる。

おまけ:アプリのバージョン差に関する挙動



バックアップ時のアプリのバージョンは、AndroidManifest.xml内のandroid:versionCodeの値が自動的に記録される。
デフォルトだと、リストア時のandroid:versionCodeがバックアップ時のそれより低いと、onRestoreが呼ばれない。
アプリがダウングレードされているため、新しい仕様のデータでリストアするのは不適切、ということだろうか。

これを避けるためには、applicationタグに

android:restoreAnyVersion="true"

と設定するといい。まあ良し悪しあるかと。

おまけ:サンプル


公式のサンプル、リンク無くなってるんじゃない...?
現時点ではここからtgzでゲットできる。
AndroidManifestなんかは書き換えないと動かないよ。