Tuesday, May 1, 2012

Do that Back Up Thing

One thing I really hate is when I play through most of a game, and then something happens where I either update my device's OS or have to reset it to factory settings and ALL of my game progress is lost. This can be easily prevented, and is not only applicable to games! Any app that has user specific data should be utilizing the Android Backup Service APIs to allow a user to have their information backed up to the cloud.

While most devices support some backup service, it's not guaranteed to be Google's backup service. However, you only need to register for Google's service. If some devices uses a different service they will have all the framework for it done internally. Also, you do not need to do anything special for devices that do not support any backup service, as your BackupAgent will just never be run on those devices.

First thing you need to do is get a Backup Service Key for the Google Android Backup Service, since a majority of devices will use this service you should definitely be getting a key. You can test your backup and restore code without the key using any emulator running Android 2.2 or higher.

Next, you need to decide which class you're going to extend: BackupAgent or BackupAgentHelper. The latter is probably the choice you will want to go with, unless you meet any of these conditions:

  • Version your data format. i.e. if you need to change the data between app versions.
  • Need to backup data in a database.
  • Only want to backup pieces of data from a file, and not the entire file
If you don't meet any of these conditions, you will want to extend BackupAgentHelper and implement any number of BackupHelper classes to do your backup work. If you are simply backing up SharedPreferences or any internal File (NOT an SQLite DB file!) there are some pre-made BackupHelpers that will do the trick. Namely, SharedPreferencesBackupHelper and FileBackupHelper, respectively. Once you have your BackupHelpers, you just add them to your BackupAgentHelper in its onCreate() method like so:
   @Override
   public void onCreate() {
      MyBackupHelper helper = new MyBackupHelper(this);
      addHelper(UNIQUE_KEY, helper);
   }
Note that we passed this as a constructor parameter. I implied that you want to do this because BackupAgent and BackupAgentHelper both extend from Context, which can be very useful to have in your BackupHelper class.

Backing up data

Both classes handle backup and restore a little differently, but the concepts are the same. During backup, you write data to the BackupDataOutput object by using the writeEntityHeader() and writeEntityData() methods of the BackupDataOutput object. You will need a unique string identifier for the header, as well as the size of the data you will be writing. If you are implementing a BackupAgent you will override onBackup() and if you are implementing one or more BackupHelper classes you will override performBackup(). Both methods have the same parameter signature. First, a ParcelFileDescriptor that represents the locally store state of the backup which you use this to determine if you even need to backup any data. Second, the BackupDataOutput object that you write your data to using the aforementioned methods. Lastly, another ParcelFileDescriptor that represents the new state of the backup, which will be passed as the first parameter the next time a backup is requested. This local state does not need to be the actual data itself, but a representation of it. You could put a version number, a timestamp or just a keycode in there. Here is a simple example of an onBackup() method:

@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
   // Get our timestamp that we use as our local representation
   long modified = mDataFile.lastModified(); // Some local data file that we modify

   FileInputStream instream = new FileInputStream(oldState.getFileDescriptor());
   DataInputStream in = new DataInputStream(instream);
   
   try {
       // Get the last modified timestamp from the state file and data file
       long stateModified = in.readLong();
       
       if (stateModified != modified) {
           // The file has been modified, so do a backup
           // Or the time on the device changed, so be safe and do a backup
           doOurBackup(data);
       } else {
        // Don't back up because the file hasn't changed
        return;
       }
   } catch (IOException e) {
      // Unable to read state file... be safe and do a backup
       doOurBackup(data);
   } finally {
      in.close(); 
      instream.close();
   }
   // now we write our "local state" as the timestamp of the time our data file was last modified
   FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
   DataOutputStream out = new DataOutputStream(outstream);
   
   out.writeLong(modified);
   out.close(); outstream.close();
}

// Use this method to perform the actual backup
private void doOurBackup(BackupDataOutput data) {
   // Create buffer stream and data output stream for our data
   ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
   DataOutputStream outWriter = new DataOutputStream(bufStream);
   // Write structured data, we will read it in the same order in onRestore()
   outWriter.writeUTF(mPlayerName);
   outWriter.writeInt(mPlayerScore);
   // Send the data to the Backup Manager via the BackupDataOutput
   byte[] buffer = bufStream.toByteArray();
   int len = buffer.length;
   data.writeEntityHeader(TOPSCORE_BACKUP_KEY, len); // Using our unique id key
   data.writeEntityData(buffer, len);
} 
Note that we do not have to close the data, newState or oldState streams, the Backup Manager will take care of that. You can write as many entities as needed, but know that each entity may not be read in the same order as you write them (in my experience it seems to follow a FILO method).

Performing Restore

The two different Backup implementations are much more similar than the restore implementations. The BackupAgent onRestore() method is only called once, and you must iterate through the data entities that you wrote on your backup. Any of your BackupHelper classes will have their restoreEntity() method called for EACH entity written by that BackupHelper. If you are using the latter approach make sure you do not read more than the size of data you wrote, as you will cause big problems. Once we know which entity we have, restoring the data is essentially the same.

@Override
public void onRestore(BackupDataInput data, int appVersionCode,
                      ParcelFileDescriptor newState) throws IOException {
    // If you're using a BackupHelper you would just have restoreEntity(BackupDataInput data)
    // and not be iterating over the headers
    while (data.readNextHeader()) {
        String key = data.getKey();
        int dataSize = data.getDataSize();

        // Figure out what unique key this header has (we only had one key)
        if (TOPSCORE_BACKUP_KEY.equals(key)) {
            // Create an input stream for the BackupDataInput
            byte[] dataBuf = new byte[dataSize];
            data.readEntityData(dataBuf, 0, dataSize);
            ByteArrayInputStream baStream = new ByteArrayInputStream(dataBuf);
            DataInputStream in = new DataInputStream(baStream);

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

            // Record the score on the device (to a file or something)
            recordScore(mPlayerName, mPlayerScore);
            // Should really be using a try/catch for IOExceptions
            in.close();
            baStream.close();
        } else {
            // We don't know this entity key. Skip it. (Shouldn't happen.)
            // But is important to do just in case, so we don't have the cursor misaligned
            data.skipEntityData();
        }
    }

    // Finally, write to the state blob (newState) that describes the restored data
    // If you're using a BackupHelper, the helper's writeNewStateDescription(ParcelFileDescriptor newState) 
    // method will be called after all the entities are read 
    FileOutputStream outstream = new FileOutputStream(newState.getFileDescriptor());
    DataOutputStream out = new DataOutputStream(outstream);
    out.writeUTF(mPlayerName);
    out.writeInt(mPlayerScore);
    // again, should use try/catch
    out.close(); outstream.close();
}
As noted in the comments, the BackupHelper works slightly different but the restoration and local state recording are the same, it is just a matter of how they are done. Now that we have our code to perform the backup and restore, we just need to register our BackupAgent in our AndroidManifest.xml and make sure we request backups from the Backup Manager when our data changes.

Registering your BackupAgent (or BackupAgentHelper)

There are really two steps for this, first you tell your app the class name of your backup agent in the application tag of the manifest, and then you register your Google Backup Service Key with a meta-data tag. Here is an example AndroidManifest.xml with both elements:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="us.sleepybear.example.backup" android:versionCode="1"
 android:versionName="3.14">
 <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="15" />


 <application android:icon="@drawable/icon" android:label="@string/app_name" 
            android:backupAgent=".MyBackUpAgent">
        <activity android:name=".MainActivity" android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!-- Backup Data Service API Key -->
        <meta-data android:name="com.google.android.backup.api_key"
                   android:value="{google-backup-api-key}" />
 </application>
</manifest>

Requesting a Backup

We're all set to utilize the BackupManager to make sure we save our user specific data to the cloud. Now we just have to request a backup to be done when our data changes. So anywhere in your app that you change your data you just need to call:

new BackupManager(getApplicationContext()).dataChanged();
As you may have noticed, BackupManager requires a Context to be able to run, so that is something to keep in mind. You can test your backup code without a Google API key nor having dataChanged() calls in your code using adb and an emulator or device with Android 2.2 or higher (Google Play must be on the device if you use it, but you don't have to use a Google APIs emulator image).

Now that you have the Backup Service working, your users will not get frustrated when they have to uninstall and re-install your app, or have to perform a factory reset or OS upgrade. This service should NOT be used as a cloud syncing service between devices, but it can allow for initialization of your app when installed on a second device owned by one user. If you want something to be synced between devices you should be using Android Cloud to Device Messaging (C2DM)

5 comments:

  1. Your backup code is slightly incorrect. You shouldn't close any streams (it explicitly states that in the docs). Anyways, any idea what writeNewStateDescription() should do?

    ReplyDelete
    Replies
    1. You should not close any of the streams that are passed in (oldState, data, newState), but any other streams you create should be closed to prevent memory leaks.

      writeNewStateDescription() is used after a restore operation is completed to write the current state of your backup. How you use it is up to you. You can put a date/time of the last data change, you can put date/time of last data backup, you can put a specific hashkey, etc. The next time a backup is requested, you can read this state and use it to verify if a backup is even required (perhaps no data has changed since last time a backup was requested).

      Delete
  2. Thank you for your post. I have a question. Will your code do backup your data (player info) onto Google cloud or on local phone/tablet storage? Can you share me the complete source code of your example? Thanks!

    ReplyDelete
    Replies
    1. This will use the cloud tied to the user's google account. This way when they get a new phone and re-install your application then it will restore the data back to where it was. This should really be utilized more in the gaming world, because I hate having to start my progress over just because I got a new phone. You should also try to save as little data as you can get away with to save the state, since there is a limited amount of space available and you don't want to always be using your user's data transfer.

      This is not the same as C2DM for syncing across all devices, you would want to implement that separately.

      All the code I wrote for this example is posted here, but you can also check out the google API and examples on the dev site, which has actually gotten a lot better since I wrote this post: http://developer.android.com/guide/topics/data/backup.html

      Delete
  3. Oh, very useful codes, if it possible, I want to use it in my dissertation.
    Thx for this article, good luck.

    Best regards
    Toby, ideals

    ReplyDelete