Merge "Add a test app: quota exceeded doing a backup" into honeycomb-mr1

This commit is contained in:
Christopher Tate
2011-03-11 18:22:37 -08:00
committed by Android (Google) Code Review
7 changed files with 652 additions and 0 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
-keepclassmembers class com.android.hugebackup.HugeBackupActivity {
public void onRestoreButtonClick(android.view.View);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}
);
}
}