diff --git a/tests/HugeBackup/Android.mk b/tests/HugeBackup/Android.mk
new file mode 100644
index 0000000000000..4789bc8118f2f
--- /dev/null
+++ b/tests/HugeBackup/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := HugeBackup
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_PACKAGE)
diff --git a/tests/HugeBackup/AndroidManifest.xml b/tests/HugeBackup/AndroidManifest.xml
new file mode 100644
index 0000000000000..923881b9b0b03
--- /dev/null
+++ b/tests/HugeBackup/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
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 (HugeBackupActivity.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 is 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);
+
+ // ***** pathological behavior *****
+ // Now, in order to incur deliberate too-much-data failures,
+ // try to back up 20 MB of data besides what we already pushed.
+ final int MEGABYTE = 1024*1024;
+ final int NUM_MEGS = 20;
+ buffer = new byte[MEGABYTE];
+ data.writeEntityHeader(HUGE_DATA_KEY, NUM_MEGS * MEGABYTE);
+ for (int i = 0; i < NUM_MEGS; i++) {
+ data.writeEntityData(buffer, MEGABYTE);
+ }
+ }
+
+ // 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 true if the application's data has changed since
+ * the last backup operation; false 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 newer 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.
+ *
+ *
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 (HugeBackupActivity.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/tests/HugeBackup/src/com/android/hugebackup/HugeBackupActivity.java b/tests/HugeBackup/src/com/android/hugebackup/HugeBackupActivity.java new file mode 100644 index 0000000000000..84e31aa8417e1 --- /dev/null +++ b/tests/HugeBackup/src/com/android/hugebackup/HugeBackupActivity.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2011 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.android.hugebackup; + +import android.app.Activity; +import android.app.backup.BackupManager; +import android.app.backup.RestoreObserver; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.RadioGroup; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * Deliberately back up waaaaaaay too much data. Cloned with some alterations + * from the Backup/Restore sample application. + */ +public class HugeBackupActivity extends Activity { + static final String TAG = "HugeBackupActivity"; + + /** + * 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. + * + *
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(), HugeBackupActivity.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 (HugeBackupActivity.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 (HugeBackupActivity.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(); + } + + /** + * Click handler, designated in the layout, that runs a restore of the app's + * most recent data when the button is pressed. + */ + public void onRestoreButtonClick(View v) { + Log.v(TAG, "Requesting restore of our most recent data"); + mBackupManager.requestRestore( + new RestoreObserver() { + public void restoreFinished(int error) { + /** Done with the restore! Now draw the new state of our data */ + Log.v(TAG, "Restore finished, error = " + error); + populateUI(); + } + } + ); + } +}