In the first article in this series we had a first look at Android Data Backup. We got a simple app working which successfully stored its SharedPreferences in the cloud and restored them we the app was re-installed. Thee was one problem, however: If we manually triggered a restore of the SharedPreferences, we did not receive a callback when the restore occurred despite registering an OnSharedPreferenceChangeListener. Let’s discover why…
I’ve said it before that one of the great things about Android is that if you hit a problem, you can look at the source code of the OS itself which can be invaluable in understanding why something doesn’t work as you expect. Let’s have a look at the source of SharedPreferencesBackupHelper on the GitHub mirror of the AOSP sources. If we look at the performBackup and restoreEntity methods it becomes clear that SharedPreferencesBackupHelper works by backing up the file that our SharedPreferences are written to, and does not use a SharedPreferences object directly at all. Our OnSharedPreferenceChangeListener will only be triggered when our SharedPreferences are updated using a SharedPreferences.Editor, which clearly isn’t happening when we use SharedPreferencesBackupHelper. This isn’t a problem when we restore during the re-installation of an app because the SharedPreferences file will be restored before the app is started for the first time. When the app is started, the restored SharedPreferences will be loaded, so everything works as it should.
Let’s come back to the problem that we saw in the previous article: We manually request a restore, and our OnSharedPreferenceChangeListener does not get called. How can we detect that our SharedPreferences have changed? One way would be to override onRestore()
in our PrefsBackupAgent class, call super.onRestore()
, and then trigger our Activity to refresh its SharePreferences. That would certainly be the easiest solution, but for this article we’ll actually implement a custom BackupAgent rather than relying on SharedPreferencesBackupHelper to do the heavy lifting for us. This will enable us to solve our problem, but will also provide some insight in to how to implement a BackupAgent if the data that you wish to store is not neatly stored in SharedPreferences.
Let’s create a new class named SaBackupAgent which extends BackupAgent:
[java] public class SaBackupAgent extends BackupAgent{
private static final String BACKUP_KEY = “BACKUP_KEY”;
private void writeNewState( String val,
ParcelFileDescriptor newState )
throws IOException
{
FileOutputStream fos = new FileOutputStream(
newState.getFileDescriptor() );
DataOutputStream dos = new DataOutputStream( fos );
dos.writeUTF( val );
dos.close();
}
}
[/java]
writeNewState()
is a utility method that we’ll use shortly. We need to override two methods in BackupAgent, onBackup()
and onRestore()
. Let’s look at onBackup()
first:
public void onBackup( ParcelFileDescriptor oldState,
BackupDataOutput data,
ParcelFileDescriptor newState ) throws IOException
{
Log.d( BackupRestoreActivity.TAG, “onBackup” );
SharedPreferences sp = getSharedPreferences(
BackupRestoreActivity.PREFS, MODE_PRIVATE );
if( sp.contains( BackupRestoreActivity.KEY ) )
{
String val = sp.getString(
BackupRestoreActivity.KEY,
null );
String oldVal = null;
if( val != null )
{
FileInputStream fis = new FileInputStream(
oldState.getFileDescriptor() );
DataInputStream dis =
new DataInputStream( fis );
try
{
oldVal = dis.readUTF();
}
catch( Exception e )
{
oldVal = null;
}
dis.close();
if( oldVal == null || !oldVal.equals( val ) )
{
ByteArrayOutputStream baos =
new ByteArrayOutputStream();
DataOutputStream dos =
new DataOutputStream( baos );
dos.writeUTF( val );
dos.close();
byte[] buf = baos.toByteArray();
data.writeEntityHeader(
BACKUP_KEY, buf.length );
data.writeEntityData( buf, buf.length );
}
writeNewState( val, newState );
}
}
}
[/java]
For this example, we’re only concerned about backing up a single key/value pair from our SharedPreferences, but the same principle that we explain here can easily be expanded. onBackup() takes three arguments: Two ParcelFileDescriptor objects named oldState
and newState
which represent the state of the backup both before and after onBackup()
is called; and a BackupDataOutput object named data
which is where we need to write the data which is to be backed up.
This may all look rather complex, but it is designed to only backup data when it has actually changed. We do this by comparing the current state of our data with oldState
, and only actually writing to data
if something has changed. In our case, we read the value of the key/value pair from our SharedPreferences, and then read a String value from oldState
. If there was no value in oldState
, or the value that we read does not match the value that we read from SharedPreferences, then we write it to data
. If we don’t write anything to data
(when we do not detect any change) then no backup will be performed. Finally we call our writeNewState()
utility method which will write the current state to newState
.
It is important that we update newState
because the Backup system will store this as the current state of the backup, and pass it to onBackup()
as oldState
the next time it is invoked. If we do not update this correctly, then the next call to onBackup()
may find a delta between the oldState
and the current state and cause an unnecessary backup. This is bad for two reasons: Firstly, it will put unnecessary load on the cloud backup service which may result in your app getting banned; Secondly, it will increase data usage which may cost your user money.
Next let’s look at onRestore()
:
public void onRestore( BackupDataInput data,
int appVersionCode,
ParcelFileDescriptor newState ) throws IOException
{
Log.d( BackupRestoreActivity.TAG, “onRestore” );
String val = null;
while( data.readNextHeader() )
{
String key = data.getKey();
int size = data.getDataSize();
if( key.equals( BACKUP_KEY ) )
{
byte[] buf = new byte[ size ];
data.readEntityData( buf, 0, size );
ByteArrayInputStream bais =
new ByteArrayInputStream( buf );
DataInputStream dis =
new DataInputStream( bais );
val = dis.readUTF();
dis.close();
}
else
{
data.skipEntityData();
}
}
if( val != null )
{
writeNewState( val, newState );
SharedPreferences sp = getSharedPreferences(
BackupRestoreActivity.PREFS, MODE_PRIVATE );
SharedPreferences.Editor editor = sp.edit();
editor.putString( BackupRestoreActivity.KEY, val );
editor.commit();
}
}
[/java]
If you understand what onBackup()
is doing then onRestore()
should be fairly easy to understand: we read our key/value pair from the BackupDataInput object, call our writeNewState()
utility method to update newState
, and then write the restored value to SharedPreferences using a SharedPrefernces.Editor. This will cause our OnSharePreferencesChangedListener to be invoked.
Once again, we must update newState
during onRestore()
as that will be stored as the current backup state, and will be passed to the new onBackup() call as oldState
.
Another thing worth mentioning is the appVersionCode
argument. This can be used to manage migration from an earlier version of your app. If the data that you store using the Backup API changes from one version to the next, you can detect a change in the app version, and correctly manage the data migration to your new version.
As a small aside, Google Developer Advocate Nick Butcher suggested using editor.apply()
instead of editor.commit()
here. Nick is, of course, correct but I wanted to keep the code backwardly compatible as far as possible and not introduce any complexity that was not required to show how to use the Backup API. In production code, I would be inclined to wrap this in a conditional block which checks the OS version and uses the most appropriate method:
{
editor.apply()
}
else
{
editor.commit();
}
[/java]
To use the new BackupAgent we need to substitute android:backupAgent
in our Manifest:
.
.
.
.
.
[/xml]
One word of warning: Changing the way that you store data (i.e. by switching from the previous SharedPreferencesBackupHelper to the BackupAgent implementation will result in different data schema being stored to the cloud. If you do not change the version of your app when you change the data schema, you can encounter problems because the cloud schema is out of sync with your current implementation. Sometimes it can be very difficult to resolve these problems, and you see exceptions thrown during the restore process even after performing a new backup using the new schema.
One important thing to mention is that if you decide to implement backup in to your app, you really should provide the user with the ability to disable it. Your user may have privacy concerns with their data being stored in the cloud, or may have a mobile phone tariff where data usage is expensive. Allowing backup to be disabled will keep such users happy. You should also consider detecting if the device is currently roaming, and only perform backups on the user’s home network as data roaming charges can be quit high.
Finally, it is worth mentioning security. You should consider encrypting any sensitive data which is stored outside of your app. This applies not only to the data that is backed up to the cloud using the backup API, but also anything that you store to SharedPreferences or persistent storage on the device (both of which may be able to read by other apps, particularly on rooted devices). A full discussion on this is beyond the scope of this article, but is worth bearing in mind, nonetheless.
That concludes our exploration of the Backup API: It is relatively easy to implement, but can really enhance your users’ experience of moving your app to a new device.
The source code for this article can be found here.
© 2012, Mark Allison. All rights reserved.
Copyright © 2012 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Hi,
I really found your tutorial great. It helping me a lot. I have one issue which i think you can solve it. Can i detect that particular device supports cloud backup function? as google docs says that data backup feature provision is up to the vendor of the devices. Please guide me if i am going in wrong direction