Backup/Restore sample application
A very simple application: one activity, with a few bits of persistent data that
are updated live by the activity. To that are added a few alternative agent
implementations, to illustrate various approaches and tradeoffs when implementing
a backup/restore agent. In particular, there are example agents that do everything
"by hand," either record-by-record or by directly backing up the app's entire
persistent data file; plus an example agent that illustrates how very easy it
is to implement the latter using the OS-provided helpers.
Part of bug #2545514
Change-Id: Iaca33a5113406360c23d6e2e59eb012f9f1e9400
diff --git a/samples/BackupRestore/Android.mk b/samples/BackupRestore/Android.mk
new file mode 100644
index 0000000..c164a6c
--- /dev/null
+++ b/samples/BackupRestore/Android.mk
@@ -0,0 +1,13 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := samples
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BackupRestore
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/samples/BackupRestore/AndroidManifest.xml b/samples/BackupRestore/AndroidManifest.xml
new file mode 100644
index 0000000..8c37720
--- /dev/null
+++ b/samples/BackupRestore/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Declare the contents of this Android application. The namespace
+ attribute brings in the Android platform namespace, and the package
+ supplies a unique name for the application. When writing your
+ own application, the package name must be changed from "com.example.*"
+ to come from a domain that you own or have control over. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.backuprestore"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <!-- The backup/restore mechanism was introduced in API version 8 -->
+ <uses-sdk android:minSdkVersion="8"/>
+
+ <application android:label="Backup/Restore"
+ android:backupAgent="ExampleAgent">
+
+ <activity android:name="BackupRestoreActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+</manifest>
diff --git a/samples/BackupRestore/res/layout/backup_restore.xml b/samples/BackupRestore/res/layout/backup_restore.xml
new file mode 100644
index 0000000..1459d42
--- /dev/null
+++ b/samples/BackupRestore/res/layout/backup_restore.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout description of the BackupRestore sample's main activity -->
+
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView android:text="@string/filling_text"
+ android:textSize="20dp"
+ android:layout_marginTop="20dp"
+ android:layout_marginBottom="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <RadioGroup android:id="@+id/filling_group"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="20dp"
+ android:orientation="vertical">
+
+ <RadioButton android:id="@+id/bacon"
+ android:text="@string/bacon_label"/>
+ <RadioButton android:id="@+id/pastrami"
+ android:text="@string/pastrami_label"/>
+ <RadioButton android:id="@+id/hummus"
+ android:text="@string/hummus_label"/>
+
+ </RadioGroup>
+
+ <TextView android:text="@string/extras_text"
+ android:textSize="20dp"
+ android:layout_marginTop="20dp"
+ android:layout_marginBottom="10dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <CheckBox android:id="@+id/mayo"
+ android:text="@string/mayo_text"
+ android:layout_marginLeft="20dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <CheckBox android:id="@+id/tomato"
+ android:text="@string/tomato_text"
+ android:layout_marginLeft="20dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+</ScrollView>
\ No newline at end of file
diff --git a/samples/BackupRestore/res/values/strings.xml b/samples/BackupRestore/res/values/strings.xml
new file mode 100644
index 0000000..64cd7a2
--- /dev/null
+++ b/samples/BackupRestore/res/values/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <string name="filling_text">Choose a sandwich filling:</string>
+ <string name="bacon_label">Bacon</string>
+ <string name="pastrami_label">Pastrami</string>
+ <string name="hummus_label">Hummus</string>
+
+ <string name="extras_text">Extras:</string>
+ <string name="mayo_text">Mayonnaise\?</string>
+ <string name="tomato_text">Tomato\?</string>
+</resources>
diff --git a/samples/BackupRestore/src/com/example/android/backuprestore/BackupRestoreActivity.java b/samples/BackupRestore/src/com/example/android/backuprestore/BackupRestoreActivity.java
new file mode 100644
index 0000000..01c10ae
--- /dev/null
+++ b/samples/BackupRestore/src/com/example/android/backuprestore/BackupRestoreActivity.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.backuprestore;
+
+import android.app.Activity;
+import android.app.backup.BackupManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.RadioGroup;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * This example is intended to demonstrate a few approaches that an Android
+ * application developer can take when implementing a
+ * {@link android.app.backup.BackupAgent BackupAgent}. This feature, added
+ * to the Android platform with API version 8, allows the application to
+ * back up its data to a device-provided storage location, transparently to
+ * the user. If the application is uninstalled and then reinstalled, or if
+ * the user starts using a new Android device, the backed-up information
+ * can be provided automatically when the application is reinstalled.
+ *
+ * <p>Participating in the backup/restore mechanism is simple. The application
+ * provides a class that extends {@link android.app.backup.BackupAgent}, and
+ * overrides the two core callback methods
+ * {@link android.app.backup.BackupAgent#onBackup(android.os.ParcelFileDescriptor, android.app.backup.BackupDataOutput, android.os.ParcelFileDescriptor) onBackup()}
+ * and
+ * {@link android.app.backup.BackupAgent#onRestore(android.app.backup.BackupDataInput, int, android.os.ParcelFileDescriptor) onRestore()}.
+ * It also publishes the agent class to the operating system by naming the class
+ * with the <code>android:backupAgent</code> attribute of the
+ * <code><application></code> tag in the application's manifest.
+ * When a backup or restore operation is performed, the application's agent class
+ * is instantiated within the application's execution context and the corresponding
+ * method invoked. Please see the documentation on the
+ * {@link android.app.backup.BackupAgent BackupAgent} class for details about the
+ * data interchange between the agent and the backup mechanism.
+ *
+ * <p>This example application maintains a few pieces of simple data, and provides
+ * three different sample agent implementations, each illustrating an alternative
+ * approach. The three sample agent classes are:
+ *
+ * <p><ol type="1">
+ * <li>{@link ExampleAgent} - this agent backs up the application's data in a single
+ * record. It illustrates the direct "by hand" processes of saving backup state for
+ * future reference, sending data to the backup transport, and reading it from a restore
+ * dataset.</li>
+ * <li>{@link FileHelperExampleAgent} - this agent takes advantage of the suite of
+ * helper classes provided along with the core BackupAgent API. By extending
+ * {@link android.app.backup.BackupHelperAgent} and using the targeted
+ * {link android.app.backup.FileBackupHelper FileBackupHelper} class, it achieves
+ * the same result as {@link ExampleAgent} - backing up the application's saved
+ * data file in a single chunk, and restoring it upon request -- in only a few lines
+ * of code.</li>
+ * <li>{@link MultiRecordExampleAgent} - this agent stores each separate bit of data
+ * managed by the UI in separate records within the backup dataset. It illustrates
+ * how an application's backup agent can do selective updates of only what information
+ * has changed since the last backup.</li></ol>
+ *
+ * <p>You can build the application to use any of these agent implementations simply by
+ * changing the class name supplied in the <code>android:backupAgent</code> manifest
+ * attribute to indicate the agent you wish to use. <strong>Note:</strong> the backed-up
+ * data and backup-state tracking of these agents are not compatible! If you change which
+ * agent the application uses, you should also wipe the backup state associated with
+ * the application on your handset. The 'bmgr' shell application on the device can
+ * do this; simply run the following command from your desktop computer while attached
+ * to the device via adb:
+ *
+ * <p><code>adb shell bmgr wipe com.example.android.backuprestore</code>
+ *
+ * <p>You can then install the new version of the application, and its next backup pass
+ * will start over from scratch with the new agent.
+ */
+public class BackupRestoreActivity extends Activity {
+ static final String TAG = "BRActivity";
+
+ /**
+ * We serialize access to our persistent data through a global static
+ * object. This ensures that in the unlikely event of the our backup/restore
+ * agent running to perform a backup while our UI is updating the file, the
+ * agent will not accidentally read partially-written data.
+ *
+ * <p>Curious but true: a zero-length array is slightly lighter-weight than
+ * merely allocating an Object, and can still be synchronized on.
+ */
+ static final Object[] sDataLock = new Object[0];
+
+ /** Also supply a global standard file name for everyone to use */
+ static final String DATA_FILE_NAME = "saved_data";
+
+ /** The various bits of UI that the user can manipulate */
+ RadioGroup mFillingGroup;
+ CheckBox mAddMayoCheckbox;
+ CheckBox mAddTomatoCheckbox;
+
+ /** Cache a reference to our persistent data file */
+ File mDataFile;
+
+ /** Also cache a reference to the Backup Manager */
+ BackupManager mBackupManager;
+
+ /** Set up the activity and populate its UI from the persistent data. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ /** Establish the activity's UI */
+ setContentView(R.layout.backup_restore);
+
+ /** Once the UI has been inflated, cache the controls for later */
+ mFillingGroup = (RadioGroup) findViewById(R.id.filling_group);
+ mAddMayoCheckbox = (CheckBox) findViewById(R.id.mayo);
+ mAddTomatoCheckbox = (CheckBox) findViewById(R.id.tomato);
+
+ /** Set up our file bookkeeping */
+ mDataFile = new File(getFilesDir(), BackupRestoreActivity.DATA_FILE_NAME);
+
+ /** It is handy to keep a BackupManager cached */
+ mBackupManager = new BackupManager(this);
+
+ /**
+ * Finally, build the UI from the persistent store
+ */
+ populateUI();
+ }
+
+ /**
+ * Configure the UI based on our persistent data, creating the
+ * data file and establishing defaults if necessary.
+ */
+ void populateUI() {
+ RandomAccessFile file;
+
+ // Default values in case there's no data file yet
+ int whichFilling = R.id.pastrami;
+ boolean addMayo = false;
+ boolean addTomato = false;
+
+ /** Hold the data-access lock around access to the file */
+ synchronized (BackupRestoreActivity.sDataLock) {
+ boolean exists = mDataFile.exists();
+ try {
+ file = new RandomAccessFile(mDataFile, "rw");
+ if (exists) {
+ Log.v(TAG, "datafile exists");
+ whichFilling = file.readInt();
+ addMayo = file.readBoolean();
+ addTomato = file.readBoolean();
+ Log.v(TAG, " mayo=" + addMayo
+ + " tomato=" + addTomato
+ + " filling=" + whichFilling);
+ } else {
+ // The default values were configured above: write them
+ // to the newly-created file.
+ Log.v(TAG, "creating default datafile");
+ writeDataToFileLocked(file,
+ addMayo, addTomato, whichFilling);
+
+ // We also need to perform an initial backup; ask for one
+ mBackupManager.dataChanged();
+ }
+ } catch (IOException ioe) {
+
+ }
+ }
+
+ /** Now that we've processed the file, build the UI outside the lock */
+ mFillingGroup.check(whichFilling);
+ mAddMayoCheckbox.setChecked(addMayo);
+ mAddTomatoCheckbox.setChecked(addTomato);
+
+ /**
+ * We also want to record the new state when the user makes changes,
+ * so install simple observers that do this
+ */
+ mFillingGroup.setOnCheckedChangeListener(
+ new RadioGroup.OnCheckedChangeListener() {
+ public void onCheckedChanged(RadioGroup group,
+ int checkedId) {
+ // As with the checkbox listeners, rewrite the
+ // entire state file
+ Log.v(TAG, "New radio item selected: " + checkedId);
+ recordNewUIState();
+ }
+ });
+
+ CompoundButton.OnCheckedChangeListener checkListener
+ = new CompoundButton.OnCheckedChangeListener() {
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ // Whichever one is altered, we rewrite the entire UI state
+ Log.v(TAG, "Checkbox toggled: " + buttonView);
+ recordNewUIState();
+ }
+ };
+ mAddMayoCheckbox.setOnCheckedChangeListener(checkListener);
+ mAddTomatoCheckbox.setOnCheckedChangeListener(checkListener);
+ }
+
+ /**
+ * Handy helper routine to write the UI data to a file.
+ */
+ void writeDataToFileLocked(RandomAccessFile file,
+ boolean addMayo, boolean addTomato, int whichFilling)
+ throws IOException {
+ file.setLength(0L);
+ file.writeInt(whichFilling);
+ file.writeBoolean(addMayo);
+ file.writeBoolean(addTomato);
+ Log.v(TAG, "NEW STATE: mayo=" + addMayo
+ + " tomato=" + addTomato
+ + " filling=" + whichFilling);
+ }
+
+ /**
+ * Another helper; this one reads the current UI state and writes that
+ * to the persistent store, then tells the backup manager that we need
+ * a backup.
+ */
+ void recordNewUIState() {
+ boolean addMayo = mAddMayoCheckbox.isChecked();
+ boolean addTomato = mAddTomatoCheckbox.isChecked();
+ int whichFilling = mFillingGroup.getCheckedRadioButtonId();
+ try {
+ synchronized (BackupRestoreActivity.sDataLock) {
+ RandomAccessFile file = new RandomAccessFile(mDataFile, "rw");
+ writeDataToFileLocked(file, addMayo, addTomato, whichFilling);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to record new UI state");
+ }
+
+ mBackupManager.dataChanged();
+ }
+}
\ No newline at end of file
diff --git a/samples/BackupRestore/src/com/example/android/backuprestore/ExampleAgent.java b/samples/BackupRestore/src/com/example/android/backuprestore/ExampleAgent.java
new file mode 100644
index 0000000..3d93c1a
--- /dev/null
+++ b/samples/BackupRestore/src/com/example/android/backuprestore/ExampleAgent.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.backuprestore;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.os.ParcelFileDescriptor;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * This is the backup/restore agent class for the BackupRestore sample
+ * application. This particular agent illustrates using the backup and
+ * restore APIs directly, without taking advantage of any helper classes.
+ */
+public class ExampleAgent extends BackupAgent {
+ /**
+ * We put a simple version number into the state files so that we can
+ * tell properly how to read "old" versions if at some point we want
+ * to change what data we back up and how we store the state blob.
+ */
+ static final int AGENT_VERSION = 1;
+
+ /**
+ * Pick an arbitrary string to use as the "key" under which the
+ * data is backed up. This key identifies different data records
+ * within this one application's data set. Since we only maintain
+ * one piece of data we don't need to distinguish, so we just pick
+ * some arbitrary tag to use.
+ */
+ static final String APP_DATA_KEY = "alldata";
+
+ /** The app's current data, read from the live disk file */
+ boolean mAddMayo;
+ boolean mAddTomato;
+ int mFilling;
+
+ /** The location of the application's persistent data file */
+ File mDataFile;
+
+ /** For convenience, we set up the File object for the app's data on creation */
+ @Override
+ public void onCreate() {
+ mDataFile = new File(getFilesDir(), BackupRestoreActivity.DATA_FILE_NAME);
+ }
+
+ /**
+ * The set of data backed up by this application is very small: just
+ * two booleans and an integer. With such a simple dataset, it's
+ * easiest to simply store a copy of the backed-up data as the state
+ * blob describing the last dataset backed up. The state file
+ * contents can be anything; it is private to the agent class, and
+ * is never stored off-device.
+ *
+ * <p>One thing that an application may wish to do is tag the state
+ * blob contents with a version number. This is so that if the
+ * application is upgraded, the next time it attempts to do a backup,
+ * it can detect that the last backup operation was performed by an
+ * older version of the agent, and might therefore require different
+ * handling.
+ */
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ // First, get the current data from the application's file. This
+ // may throw an IOException, but in that case something has gone
+ // badly wrong with the app's data on disk, and we do not want
+ // to back up garbage data. If we just let the exception go, the
+ // Backup Manager will handle it and simply skip the current
+ // backup operation.
+ synchronized (BackupRestoreActivity.sDataLock) {
+ RandomAccessFile file = new RandomAccessFile(mDataFile, "r");
+ mFilling = file.readInt();
+ mAddMayo = file.readBoolean();
+ mAddTomato = file.readBoolean();
+ }
+
+ // If the new state file descriptor is null, this is the first time
+ // a backup is being performed, so we know we have to write the
+ // data. If there <em>is</em> a previous state blob, we want to
+ // double check whether the current data is actually different from
+ // our last backup, so that we can avoid transmitting redundant
+ // data to the storage backend.
+ boolean doBackup = (oldState == null);
+ if (!doBackup) {
+ doBackup = compareStateFile(oldState);
+ }
+
+ // If we decided that we do in fact need to write our dataset, go
+ // ahead and do that. The way this agent backs up the data is to
+ // flatten it into a single buffer, then write that to the backup
+ // transport under the single key string.
+ if (doBackup) {
+ ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
+
+ // We use a DataOutputStream to write structured data into
+ // the buffering stream
+ DataOutputStream outWriter = new DataOutputStream(bufStream);
+ outWriter.writeInt(mFilling);
+ outWriter.writeBoolean(mAddMayo);
+ outWriter.writeBoolean(mAddTomato);
+
+ // Okay, we've flattened the data for transmission. Pull it
+ // out of the buffering stream object and send it off.
+ byte[] buffer = bufStream.toByteArray();
+ int len = buffer.length;
+ data.writeEntityHeader(APP_DATA_KEY, len);
+ data.writeEntityData(buffer, len);
+ }
+
+ // Finally, in all cases, we need to write the new state blob
+ writeStateFile(newState);
+ }
+
+ /**
+ * Helper routine - read a previous state file and decide whether to
+ * perform a backup based on its contents.
+ *
+ * @return <code>true</code> if the application's data has changed since
+ * the last backup operation; <code>false</code> otherwise.
+ */
+ boolean compareStateFile(ParcelFileDescriptor oldState) {
+ FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
+ DataInputStream in = new DataInputStream(instream);
+
+ try {
+ int stateVersion = in.readInt();
+ if (stateVersion > AGENT_VERSION) {
+ // Whoops; the last version of the app that backed up
+ // data on this device was <em>newer</em> than the current
+ // version -- the user has downgraded. That's problematic.
+ // In this implementation, we recover by simply rewriting
+ // the backup.
+ return true;
+ }
+
+ // The state data we store is just a mirror of the app's data;
+ // read it from the state file then return 'true' if any of
+ // it differs from the current data.
+ int lastFilling = in.readInt();
+ boolean lastMayo = in.readBoolean();
+ boolean lastTomato = in.readBoolean();
+
+ return (lastFilling != mFilling)
+ || (lastTomato != mAddTomato)
+ || (lastMayo != mAddMayo);
+ } catch (IOException e) {
+ // If something went wrong reading the state file, be safe
+ // and back up the data again.
+ return true;
+ }
+ }
+
+ /**
+ * Write out the new state file: the version number, followed by the
+ * three bits of data as we sent them off to the backup transport.
+ */
+ void writeStateFile(ParcelFileDescriptor stateFile) throws IOException {
+ FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
+ DataOutputStream out = new DataOutputStream(outstream);
+
+ out.writeInt(AGENT_VERSION);
+ out.writeInt(mFilling);
+ out.writeBoolean(mAddMayo);
+ out.writeBoolean(mAddTomato);
+ }
+
+ /**
+ * This application does not do any "live" restores of its own data,
+ * so the only time a restore will happen is when the application is
+ * installed. This means that the activity itself is not going to
+ * be running while we change its data out from under it. That, in
+ * turn, means that there is no need to send out any sort of notification
+ * of the new data: we only need to read the data from the stream
+ * provided here, build the application's new data file, and then
+ * write our new backup state blob that will be consulted at the next
+ * backup operation.
+ *
+ * <p>We don't bother checking the versionCode of the app who originated
+ * the data because we have never revised the backup data format. If
+ * we had, the 'appVersionCode' parameter would tell us how we should
+ * interpret the data we're about to read.
+ */
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+ // We should only see one entity in the data stream, but the safest
+ // way to consume it is using a while() loop
+ while (data.readNextHeader()) {
+ String key = data.getKey();
+ int dataSize = data.getDataSize();
+
+ if (APP_DATA_KEY.equals(key)) {
+ // It's our saved data, a flattened chunk of data all in
+ // one buffer. Use some handy structured I/O classes to
+ // extract it.
+ byte[] dataBuf = new byte[dataSize];
+ data.readEntityData(dataBuf, 0, dataSize);
+ ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
+ DataInputStream in = new DataInputStream(baStream);
+
+ mFilling = in.readInt();
+ mAddMayo = in.readBoolean();
+ mAddTomato = in.readBoolean();
+
+ // Now we are ready to construct the app's data file based
+ // on the data we are restoring from.
+ synchronized (BackupRestoreActivity.sDataLock) {
+ RandomAccessFile file = new RandomAccessFile(mDataFile, "rw");
+ file.setLength(0L);
+ file.writeInt(mFilling);
+ file.writeBoolean(mAddMayo);
+ file.writeBoolean(mAddTomato);
+ }
+ } else {
+ // Curious! This entity is data under a key we do not
+ // understand how to process. Just skip it.
+ data.skipEntityData();
+ }
+ }
+
+ // The last thing to do is write the state blob that describes the
+ // app's data as restored from backup.
+ writeStateFile(newState);
+ }
+}
diff --git a/samples/BackupRestore/src/com/example/android/backuprestore/FileHelperExampleAgent.java b/samples/BackupRestore/src/com/example/android/backuprestore/FileHelperExampleAgent.java
new file mode 100644
index 0000000..b7ec016
--- /dev/null
+++ b/samples/BackupRestore/src/com/example/android/backuprestore/FileHelperExampleAgent.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.backuprestore;
+
+import java.io.IOException;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FileBackupHelper;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * This agent backs up the application's data using the BackupAgentHelper
+ * infrastructure. In this application's case, the backup data is merely
+ * a duplicate of the stored data file; that makes it a perfect candidate
+ * for backing up using the {@link android.app.backup.FileBackupHelper} class
+ * provided by the Android operating system.
+ *
+ * <p>"Backup helpers" are a general mechanism that an agent implementation
+ * uses by extending {@link BackupAgentHelper} rather than the basic
+ * {@link BackupAgent} class.
+ *
+ * <p>By itself, the FileBackupHelper is properly handling the backup and
+ * restore of the datafile that we've configured it with, but it does
+ * not know about the potential need to use locking around its access
+ * to it. However, it is straightforward to override
+ * {@link #onBackup()} and {@link #onRestore()} to supply the necessary locking
+ * around the helper's operation.
+ */
+public class FileHelperExampleAgent extends BackupAgentHelper {
+ /**
+ * The "key" string passed when adding a helper is a token used to
+ * disambiguate between entities supplied by multiple different helper
+ * objects. They only need to be unique among the helpers within this
+ * one agent class, not globally unique.
+ */
+ static final String FILE_HELPER_KEY = "the_file";
+
+ /**
+ * The {@link android.app.backup.FileBackupHelper FileBackupHelper} class
+ * does nearly all of the work for our use case: backup and restore of a
+ * file stored within our application's getFilesDir() location. It will
+ * also handle files stored at any subpath within that location. All we
+ * need to do is a bit of one-time configuration: installing the helper
+ * when this agent object is created.
+ */
+ @Override
+ public void onCreate() {
+ // All we need to do when working within the BackupAgentHelper mechanism
+ // is to install the helper that will process and back up the files we
+ // care about. In this case, it's just one file.
+ FileBackupHelper helper = new FileBackupHelper(this, BackupRestoreActivity.DATA_FILE_NAME);
+ addHelper(FILE_HELPER_KEY, helper);
+ }
+
+ /**
+ * We want to ensure that the UI is not trying to rewrite the data file
+ * while we're reading it for backup, so we override this method to
+ * supply the necessary locking.
+ */
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ // Hold the lock while the FileBackupHelper performs the backup operation
+ synchronized (BackupRestoreActivity.sDataLock) {
+ super.onBackup(oldState, data, newState);
+ }
+ }
+
+ /**
+ * Adding locking around the file rewrite that happens during restore is
+ * similarly straightforward.
+ */
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+ // Hold the lock while the FileBackupHelper restores the file from
+ // the data provided here.
+ synchronized (BackupRestoreActivity.sDataLock) {
+ super.onRestore(data, appVersionCode, newState);
+ }
+ }
+}
diff --git a/samples/BackupRestore/src/com/example/android/backuprestore/MultiRecordExampleAgent.java b/samples/BackupRestore/src/com/example/android/backuprestore/MultiRecordExampleAgent.java
new file mode 100644
index 0000000..47359c7
--- /dev/null
+++ b/samples/BackupRestore/src/com/example/android/backuprestore/MultiRecordExampleAgent.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.backuprestore;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.os.ParcelFileDescriptor;
+
+/**
+ * This agent implementation is similar to the {@link ExampleAgent} one, but
+ * stores each distinct piece of application data in a separate record within
+ * the backup data set. These records are updated independently: if the user
+ * changes the state of one of the UI's checkboxes, for example, only that
+ * datum's backup record is updated, not the entire data file.
+ */
+public class MultiRecordExampleAgent extends BackupAgent {
+ // Key strings for each record in the backup set
+ static final String FILLING_KEY = "filling";
+ static final String MAYO_KEY = "mayo";
+ static final String TOMATO_KEY = "tomato";
+
+ // Current live data, read from the application's data file
+ int mFilling;
+ boolean mAddMayo;
+ boolean mAddTomato;
+
+ /** The location of the application's persistent data file */
+ File mDataFile;
+
+ @Override
+ public void onCreate() {
+ // Cache a File for the app's data
+ mDataFile = new File(getFilesDir(), BackupRestoreActivity.DATA_FILE_NAME);
+ }
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ // First, get the current data from the application's file. This
+ // may throw an IOException, but in that case something has gone
+ // badly wrong with the app's data on disk, and we do not want
+ // to back up garbage data. If we just let the exception go, the
+ // Backup Manager will handle it and simply skip the current
+ // backup operation.
+ synchronized (BackupRestoreActivity.sDataLock) {
+ RandomAccessFile file = new RandomAccessFile(mDataFile, "r");
+ mFilling = file.readInt();
+ mAddMayo = file.readBoolean();
+ mAddTomato = file.readBoolean();
+ }
+
+ // If this is the first backup ever, we have to back up everything
+ boolean forceBackup = (oldState == null);
+
+ // Now read the state as of the previous backup pass, if any
+ int lastFilling = 0;
+ boolean lastMayo = false;
+ boolean lastTomato = false;
+
+ if (!forceBackup) {
+
+ FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
+ DataInputStream in = new DataInputStream(instream);
+
+ try {
+ // Read the state as of the last backup
+ lastFilling = in.readInt();
+ lastMayo = in.readBoolean();
+ lastTomato = in.readBoolean();
+ } catch (IOException e) {
+ // If something went wrong reading the state file, be safe and
+ // force a backup of all the data again.
+ forceBackup = true;
+ }
+ }
+
+ // Okay, now check each datum to see whether we need to back up a new value. We'll
+ // reuse the bytearray buffering stream for each datum. We also use a little
+ // helper routine to avoid some code duplication when writing the two boolean
+ // records.
+ ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bufStream);
+
+ if (forceBackup || (mFilling != lastFilling)) {
+ // bufStream.reset(); // not necessary the first time, but good to remember
+ out.writeInt(mFilling);
+ writeBackupEntity(data, bufStream, FILLING_KEY);
+ }
+
+ if (forceBackup || (mAddMayo != lastMayo)) {
+ bufStream.reset();
+ out.writeBoolean(mAddMayo);
+ writeBackupEntity(data, bufStream, MAYO_KEY);
+ }
+
+ if (forceBackup || (mAddTomato != lastTomato)) {
+ bufStream.reset();
+ out.writeBoolean(mAddTomato);
+ writeBackupEntity(data, bufStream, TOMATO_KEY);
+ }
+
+ // Finally, write the state file that describes our data as of this backup pass
+ writeStateFile(newState);
+ }
+
+ /**
+ * Write out the new state file: the version number, followed by the
+ * three bits of data as we sent them off to the backup transport.
+ */
+ void writeStateFile(ParcelFileDescriptor stateFile) throws IOException {
+ FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
+ DataOutputStream out = new DataOutputStream(outstream);
+
+ out.writeInt(mFilling);
+ out.writeBoolean(mAddMayo);
+ out.writeBoolean(mAddTomato);
+ }
+
+ // Helper: write the boolean 'value' as a backup record under the given 'key',
+ // reusing the given buffering stream & data writer objects to do so.
+ void writeBackupEntity(BackupDataOutput data, ByteArrayOutputStream bufStream, String key)
+ throws IOException {
+ byte[] buf = bufStream.toByteArray();
+ data.writeEntityHeader(key, buf.length);
+ data.writeEntityData(buf, buf.length);
+ }
+
+ /**
+ * On restore, we pull the various bits of data out of the restore stream,
+ * then reconstruct the application's data file inside the shared lock. A
+ * restore data set will always be the full set of records supplied by the
+ * application's backup operations.
+ */
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+
+ // Consume the restore data set, remembering each bit of application state
+ // that we see along the way
+ while (data.readNextHeader()) {
+ String key = data.getKey();
+ int dataSize = data.getDataSize();
+
+ // In this implementation, we trust that we won't see any record keys
+ // that we don't understand. Since we expect to handle them all, we
+ // go ahead and extract the data for each record before deciding how
+ // it will be handled.
+ byte[] dataBuf = new byte[dataSize];
+ data.readEntityData(dataBuf, 0, dataSize);
+ ByteArrayInputStream instream = new ByteArrayInputStream(dataBuf);
+ DataInputStream in = new DataInputStream(instream);
+
+ if (FILLING_KEY.equals(key)) {
+ mFilling = in.readInt();
+ } else if (MAYO_KEY.equals(key)) {
+ mAddMayo = in.readBoolean();
+ } else if (TOMATO_KEY.equals(key)) {
+ mAddTomato = in.readBoolean();
+ }
+ }
+
+ // Now we're ready to write out a full new dataset for the application. Note that
+ // the restore process is intended to *replace* any existing or default data, so
+ // we can just go ahead and overwrite it all.
+ synchronized (BackupRestoreActivity.sDataLock) {
+ RandomAccessFile file = new RandomAccessFile(mDataFile, "rw");
+ file.setLength(0L);
+ file.writeInt(mFilling);
+ file.writeBoolean(mAddMayo);
+ file.writeBoolean(mAddTomato);
+ }
+
+ // Finally, write the state file that describes our data as of this restore pass.
+ writeStateFile(newState);
+ }
+}