diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/HugeBackup/Android.mk | 15 | ||||
-rw-r--r-- | tests/HugeBackup/AndroidManifest.xml | 44 | ||||
-rw-r--r-- | tests/HugeBackup/proguard.flags | 3 | ||||
-rw-r--r-- | tests/HugeBackup/res/layout/backup_restore.xml | 87 | ||||
-rw-r--r-- | tests/HugeBackup/res/values/strings.xml | 28 | ||||
-rw-r--r-- | tests/HugeBackup/src/com/android/hugebackup/HugeAgent.java | 261 | ||||
-rw-r--r-- | tests/HugeBackup/src/com/android/hugebackup/HugeBackupActivity.java | 214 |
7 files changed, 652 insertions, 0 deletions
diff --git a/tests/HugeBackup/Android.mk b/tests/HugeBackup/Android.mk new file mode 100644 index 0000000..4789bc8 --- /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 0000000..923881b --- /dev/null +++ b/tests/HugeBackup/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<!-- 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.android.hugebackup" + android:versionCode="1" + android:versionName="1.0"> + + <!-- The backup/restore mechanism was introduced in API version 8 --> + <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="8" /> + + <application android:label="Huge Backup" + android:backupAgent="HugeAgent"> + + <meta-data android:name="com.google.android.backup.api_key" + android:value="AEdPqrEAAAAINyoagzQOEEpIH3yw7LYCFN7CRX4FMd6TGIGVaA" /> + + <activity android:name="HugeBackupActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> +</manifest> diff --git a/tests/HugeBackup/proguard.flags b/tests/HugeBackup/proguard.flags new file mode 100644 index 0000000..b4d01bf --- /dev/null +++ b/tests/HugeBackup/proguard.flags @@ -0,0 +1,3 @@ +-keepclassmembers class com.android.hugebackup.HugeBackupActivity { + public void onRestoreButtonClick(android.view.View); +} diff --git a/tests/HugeBackup/res/layout/backup_restore.xml b/tests/HugeBackup/res/layout/backup_restore.xml new file mode 100644 index 0000000..7f11984 --- /dev/null +++ b/tests/HugeBackup/res/layout/backup_restore.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<!-- Layout description of the BackupRestore sample's main activity --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ScrollView + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1"> + + <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> + + <Button android:id="@+id/restore_button" + android:text="@string/restore_text" + android:onClick="onRestoreButtonClick" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_weight="0" /> + +</LinearLayout> diff --git a/tests/HugeBackup/res/values/strings.xml b/tests/HugeBackup/res/values/strings.xml new file mode 100644 index 0000000..c0b9226 --- /dev/null +++ b/tests/HugeBackup/res/values/strings.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<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> + + <string name="restore_text">Restore last data</string> +</resources> diff --git a/tests/HugeBackup/src/com/android/hugebackup/HugeAgent.java b/tests/HugeBackup/src/com/android/hugebackup/HugeAgent.java new file mode 100644 index 0000000..f90bd9f --- /dev/null +++ b/tests/HugeBackup/src/com/android/hugebackup/HugeAgent.java @@ -0,0 +1,261 @@ +/* + * 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.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 HugeAgent 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"; + static final String HUGE_DATA_KEY = "colossus"; + + /** 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(), HugeBackupActivity.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 (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 <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); + + // ***** 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 <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 (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 0000000..84e31aa --- /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. + * + * <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(), 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(); + } + } + ); + } +} |