Often, an app will need to backup it's data, especially if the data took time, effort or money to collect. The Android Backup Service provides a means of preserving data that is more complex than simple shared preferences or files.
In order to work with the backup API, we have to have registered our app package name with the Android Backup Service.
We do this on http://developer.android.com/google/backup/signup.html.
In this recipe, we will save a value to a file and read from a file. This can be a far more comprehensive set of actions on a larger dataset, but not necessary, for example:
File.WriteAllText(dataPath, clickCount.ToString());
if (File.Exists(dataPath)) { clickCount = int.Parse(File.ReadAllText(dataPath)); }
Now that we have the data moving in our app, we need to hook up the backup. Although this example uses a simple file stored locally, we can back up data in a database using an algorithm to determine whether a backup needs to be done. Let's take a look at the following steps:
[assembly: MetaData( "com.google.android.backup.api_key", Value = "AndroidBackupServiceKey")]
BackupAgent
instance:public class BackupHelper : BackupAgent { public override void OnBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { // backup logic } public override void OnRestore( BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) { // restore logic } }
OnBackup()
method in four main steps. First, we read the state or metadata associated with the last backup, if any:var stateDate = DateTime.MinValue; if (oldState != null) { using (var descr = oldState.FileDescriptor) using (var file = new FileInputStream(descr)) using (var invoker = new InputStreamInvoker(file)) using (var stream = new DataInputStream(invoker)) { stateDate = DateTime.FromBinary(stream.ReadLong()); } }
var fileDate = DateTime.MinValue; if (File.Exists(dataPath)) { fileDate = File.GetLastWriteTimeUtc(dataPath); } else { stateDate = DateTime.MinValue; }
if (stateDate != fileDate) { var buffer = File.ReadAllBytes(dataPath); data.WriteEntityHeader("DataHeader", buffer.Length); data.WriteEntityData(buffer, buffer.Length); }
using (var descr = newState.FileDescriptor) using (var file = new FileOutputStream(descr)) using (var invoker = new OutputStreamInvoker(file)) using (var stream = new DataOutputStream(invoker)) { stream.WriteLong(fileDate.ToBinary()); }
OnRestore()
method. We first have to go through all the bits of data that were backed up until we find the header we are looking for, that is, what we know our is app saved:while (data.ReadNextHeader()) { if (data.Key == "DataHeader") { var buffer = new byte[data.DataSize]; data.ReadEntityData(buffer, 0, data.DataSize); File.WriteAllBytes(dataPath, buffer); break; } else { data.SkipEntityData(); } }
if (File.Exists(dataPath)) { using (var descr = newState.FileDescriptor) using (var file = new FileOutputStream(descr)) using (var invoker = new OutputStreamInvoker(file)) using (var stream = new DataOutputStream(invoker)) { var fileDate = File.GetLastWriteTimeUtc(dataPath); stream.WriteLong(fileDate.ToBinary()); } }
We can easily back up shared preferences or entire files, but sometimes, we need to have more flexibility or control. To do this, we can create an instance of the base BackupAgent
instance.
There are several advantages of using the base agent directly as opposed to the helpers. The most notable advantage is the ability to version data and appropriately upgrade or downgrade the data. There is also the powerful ability to back up and restore partial data, such as a section of a larger file. We can also back up specific tables in a database. This will allow us to better version the data as well as reduce the size by excluding static data.
There are two stages to any backup process: backup and restore. The backup stage consists of four main steps:
oldState
parameter. This state is used to identify the backed up data but is not the data itself; it can be anything, even a simple, last-modified timestamp of the data. If the state is null
, then no backup or restore has been performed and we should take a backup.BackupDataOutput
object with a set of key-value entities. We first write a header using the WriteEntityHeader()
method, and then the actual data using the WriteEntityData()
method. If we are backing up a database table, we can read the contents into an XML document and then write that.newState
parameter. Even if nothing was backed up, we can still write a value representing the fact that nothing was done, that is, the original value. This state is passed in as the oldState
parameter the next time a backup is run. If we are backing up a table, we can write the last login date.Performing a restore is a bit easier as we only have to execute two steps:
BackupDataInput
object and work with the keys we recognize. We can loop through the headers using the ReadNextHeader()
method, and if we are working with different app versions, we may choose to ignore some using the SkipEntityData()
method. If we want to read the data, we use the ReadEntityData()
method.newState
parameter. If we are restoring a section of a file, we can use the timestamp. This state is passed in as the oldState
parameter when a backup is run.The OnBackup()
method is called when the backup manager decides that it is time to run a backup. We can request a backup using the backup manager, but cannot control when it is run. The OnRestore()
method is called by the backup manager as well, but is also automatically called when the app is installed. Like backups, we can request a restore but cannot directly control when it happens.