AndroidのData Backup

この記事は、まだ書きかけです。

前置き

AndroidのAPIレベル8(Android 2.2)で導入されたデータ・バックアップ。まだ使ったことないけど、ひとまずDev Guideを読みながらメモしておきます。

この記事は、まだ書きかけです。

本文

概要

  • データとsettingsをクラウドに保存
  • 端末リセット後や新しい端末にアプリを再インストールすると自動リストア
  • バックアップはアプリから要求できる
  • バックアップ時はandroid.app.backup.BackupManagerからアプリへデータの問い合わせが来る
  • a backup transportがクラウドへ送信する
  • リストア時は、Backup Manager→a backup transport→アプリ
  • リストアもアプリから要求できるがその必要なないだろう(システムが面倒を見るので)
  • 同期のためのサービスではない
  • Backup ManagerのAPIを使わずにデータを読み書きすることはできない
  • a backup transportは端末メーカーがカスタマイズでき、端末ごとに異なる(が、その違いをアプリが意識することはない)
  • すべての端末でバックアップサービスが使えるわけではない
  • バックアップ中のデータに別のアプリがアクセスすることはできない
  • クラウドのストレージと転送サービスのセキュリティは保障しない(GoogleのAndroid Backup Serviceなら、ちゃんとセキュリティのことも考えてある
At Google, we are keenly aware of the trust users place in us and our responsibility to protect users' privacy.
Google securely transmits backup data to and from Google servers in order to provide backup and restore features.
Google treats this data as personal information in accordance with Google's Privacy Policy.

基本

  • アプリはa backup agentを実装する
  • agentがBackup Managerからコールバックされる
  • マニフェストの<application>要素にandroid:backupAgent属性を指定
  • 使用するBackup Serviceにアプリを登録する
  • Android Backup Serviceは、多くの端末がサポートするであろう、GoogleのBackup Service
  • Android Backup Serviceは、端末のprimary Google accountを使ってデータを保存する
  • android.app.backup.BackupAgentかandroid.app.backup.BackupAgentHelperを継承してagentを実装
  • BackupAgentなら、onBackup()とonRestore()をオーバーライド
  • BackupAgentHelperなら、onBackup()とonRestore()をオーバーライドする代わりにhelper objectsを使う
  • BackupAgentHelperは、Shared PreferencesとInternal Storageのファイルをバックアップできる

マニフェスト

  • android:backupAgent属性にagentのクラス名を指定
<manifest ... >
    ...
    <application android:label="MyApplication"
                 android:backupAgent="MyBackupAgent">
        <activity ... >
            ...
        </activity>
    </application>
</manifest>
  • <application>の属性でandroid:restoreAnyVersionというのもある(true/false)

サービスに登録(GoogleのAndroid Backup Serviceを使う場合)

  • アプリを登録して、Backup Service Keyを得る
  • キーをマニフェストに設定する
  • アプリごとに別々のキーが必要
<application android:label="MyApplication"
             android:backupAgent="MyBackupAgent">
    ...
    <meta-data android:name="com.google.android.backup.api_key"
        android:value="AEdPqrEAAAAIDaYEVgU6DJnyJdBmU7KLH3kszDXLv_4DIsEIyQ" />
</application>

BackupAgentを使う

以下の場合に適している。

  • アプリのバージョンとバックアップデータのバージョンが異なる場合の変換処理を行いたい
  • ファイル全体ではなく、一部のデータをバックアップしたい
  • データベースのデータをバックアップしたい

void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)

  • バックアップ時にコールバックされる
  • oldState(android.os.ParcelFileDescriptor)は、前回のバックアップデータの状態(データそのものではなく、ダイジェストみたいなもので、端末内に保管される)
  • data(android.app.backup.BackupDataOutput)は、データを書き込むためのI/F
  • newState(android.os.ParcelFileDescriptor)には、次回のバックアップに備えて、今回のバックアップデータの状態をセットしておく
  • oldStateを見て、バックアップ要否を判断する
  • 判断方法はアプリ依存
  • 判断できるような情報を、前回のバックアップ時にnewStateへ書いておけということ

例えば、タイムスタンプとか。

// Get the oldState input stream
FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
DataInputStream in = new DataInputStream(instream);

try {
    // Get the last modified timestamp from the state file and data file
    long stateModified = in.readLong();
    long fileModified = mDataFile.lastModified();

    if (stateModified != fileModified) {
        // The file has been modified, so do a backup
        // Or the time on the device changed, so be safe and do a backup
    } else {
        // Don't back up because the file hasn't changed
        return;
    }
} catch (IOException e) {
    // Unable to read state file... be safe and do a backup
}
  • dataには、データをentity単位で書く
  • entityとは、keyとvalueのペアみたいなもの
  • BackupDataOutput#writeEntityHeader()でkeyとvalueのサイズを書き
  • BackupDataOutput#writeEntityData()でvalueを書く
// Create buffer stream and data output stream for our data
ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
DataOutputStream outWriter = new DataOutputStream(bufStream);
// Write structured data
outWriter.writeUTF(mPlayerName);
outWriter.writeInt(mPlayerScore);
// Send the data to the Backup Manager via the BackupDataOutput
byte[] buffer = bufStream.toByteArray();
int len = buffer.length;
data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len);
data.writeEntityData(buffer, len);
  • newStateに状態を書く
FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
DataOutputStream out = new DataOutputStream(outstream);

long modified = mDataFile.lastModified();
out.writeLong(modified);
  • ★バックアップ中は、排他制御を忘れずに
  • onBackup()はUIスレッド上で呼ばれるのかなぁ?

void onRestore (BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)

  • リストア時にコールバックされる
  • データを保存する
  • appVersionCodeはアプリのversionCode(マニフェストのandroid:versionCode)
  • versionCodeはバックアップ時に、自動的にクラウドに保存される
  • newStateには、状態を書く
  • newStateはR/Wなので、クラウドからDLしたデータの状態のgetもできるはず
  • data(android.app.backup.BackupDataInput)を使って、クラウドからDLしたデータを読む
  • readNextHeader()で全entitiyをiterate
  • getKey()でキーを取得
  • getDataSize()でvalueのサイズを取得
  • readEntityData()でvalueを取得
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
                      ParcelFileDescriptor newState) throws IOException {
    // There should be only one entity, but the safest
    // way to consume it is using a while loop
    while (data.readNextHeader()) {
        String key = data.getKey();
        int dataSize = data.getDataSize();

        // If the key is ours (for saving top score). Note this key was used when
        // we wrote the backup entity header
        if (TOPSCORE_BACKUP_KEY.equals(key)) {
            // Create an input stream for the BackupDataInput
            byte[] dataBuf = new byte[dataSize];
            data.readEntityData(dataBuf, 0, dataSize);
            ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
            DataInputStream in = new DataInputStream(baStream);

            // Read the player name and score from the backup data
            mPlayerName = in.readUTF();
            mPlayerScore = in.readInt();

            // Record the score on the device (to a file or something)
            recordScore(mPlayerName, mPlayerScore);
        } else {
            // We don't know this entity key. Skip it. (Shouldn't happen.)
            data.skipEntityData();
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    out.writeUTF(mPlayerName);
    out.writeInt(mPlayerScore);
}

バックアップ

  • アプリからバックアップを要求する場合は、BackupManager#dataChanged()を呼ぶ
  • Backup Managerが適当な時期を見計らってonBackup()をコールバックする
  • 開発中は、bmgrツールを使えば、バックアップを即発動できる
  • Settings→Privacy→Back up my data and Automatic restoreでバックアップをON/OFFする
  • adb shellで
# bmgr enable true
# bmgr backup your.package.name
# bmgr run

この後、アプリをuninstallして再installすると、データがリストアされる

  • リストアの場合はrun不要(bmgr restore your.package.nameのみ)
  • bmgr wipe your.package.nameでバックアップデータをクリア
  • bmgr enabledで、バックアップのON/OFF状態を見る
  • ★バックアップをOFFにすると、クラウドのデータも消える

リストア

  • アプリから要求するには、BackupManager#requestRestore()を呼ぶ
  • bmgrツール
  • クラウド上のデータのバージョンがアプリのバージョンより新しい(つまり、ユーザがアプリをダウングレードした)場合、Backup ManagerはonResutore()をコールバックしない
  • ただし、android:restoreAnyVersionがtrueならonRestore()をコールバックする
  • 自アプリのversionCodeを得る方法
PackageInfo info;
try {
    String name = getPackageName();
    info = getPackageManager().getPackageInfo(name,0);
} catch (NameNotFoundException nnfe) {
    info = null;
}

int version;
if (info != null) {
    version = info.versionCode;
}

BackupAgentHelperを使う

  • BackupAgentより簡単
  • backup helper objectsを使う
  • 現在2種類のhelper objectがサポートされている
  • Shared Preferences用のandroid.app.backup.SharedPreferencesBackupHelper
  • Internal Storage用のandroid.app.backup.FileBackupHelper
  • BackupAgentHelper#onCreate()の中で、helper objectsを生成し、addHelper()する

Shared Preferences用のagent

public class MyPrefsBackupAgent extends BackupAgentHelper {
    // The name of the SharedPreferences file
    static final String PREFS = "user_preferences";

    // A key to uniquely identify the set of backup data
    static final String PREFS_BACKUP_KEY = "prefs";

    // Allocate a helper and add it to the backup agent
    @Override
    public void onCreate() {
        SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, PREFS);
        addHelper(PREFS_BACKUP_KEY, helper);
    }
}
  • Shared Preferencesはスレッドセーフなので安心(ホンマか?)

Internal Storage用のagent

public class MyFileBackupAgent extends BackupAgentHelper {
    // The name of the Internal Storage files
    static final String TOP_SCORES = "scores";
    static final String PLAYER_STATS = "stats";

    // A key to uniquely identify the set of backup data
    static final String FILES_BACKUP_KEY = "myfiles";

    // Allocate a helper and add it to the backup agent
    void onCreate() {
        FileBackupHelper helper = new FileBackupHelper(this, TOP_SCORES, PLAYER_STATS);
        addHelper(FILES_BACKUP_KEY, helper);
    }
}

Internal Storageのファイル読み書きはスレッドセーフではないので、排他制御が必要。

  • ロックを作る
static final Object[] sDataLock = new Object[0];  // ObjectよりObject[0]の方が軽量。
  • ファイルの読み書き時にロック。
try {
    synchronized (MyActivity.sDataLock) {
        File dataFile = new File(getFilesDir(), TOP_SCORES);
        RandomAccessFile raFile = new RandomAccessFile(dataFile, "rw");
        raFile.writeInt(score);
    }
} catch (IOException e) {
    Log.e(TAG, "Unable to write to file");
}
  • バックアップ/リストア時にもロック。
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
          ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper performs backup
    synchronized (MyActivity.sDataLock) {
        super.onBackup(oldState, data, newState);
    }
}

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
        ParcelFileDescriptor newState) throws IOException {
    // Hold the lock while the FileBackupHelper restores the file
    synchronized (MyActivity.sDataLock) {
        super.onRestore(data, appVersionCode, newState);
    }
}
Last modified:2011/06/25 17:45:16
Keyword(s):
References:[Androidアプリ開発]
This page is frozen.