88

I have implemented a BackupAgentHelper using the provided FileBackupHelper to backup and restore the native database I have. This is the database you typically use along with ContentProviders and which resides in /data/data/yourpackage/databases/.

One would think this is a common case. However the docs aren't clear on what to do: http://developer.android.com/guide/topics/data/backup.html. There is no BackupHelper specifically for these typical databases. Hence I used the FileBackupHelper, pointed it to my .db file in "/databases/", introduced locks around any db operation (such as db.insert) in my ContentProviders, and even tried creating the "/databases/" directory before onRestore() because it does not exist after install.

I have implemented a similar solution for the SharedPreferences successfully in a different app in the past. However when I test my new implementation in the emulator-2.2, I see a backup being performed to LocalTransport from the logs, as well as a restore being performed (and onRestore() called). Yet, the db file itself is never created.

Note that this is all after an install, and before first launch of the app, after the restore has been performed. Apart from that my test strategy was based on http://developer.android.com/guide/topics/data/backup.html#Testing.

Please also note I'm not talking about some sqlite database I manage myself, nor about backing up to SDcard, own server or elsewhere.

I did see a mention in the docs about databases advising to use a custom BackupAgent but it does not seem related:

However, you might want to extend BackupAgent directly if you need to: * Back up data in a database. If you have an SQLite database that you want to restore when the user re-installs your application, you need to build a custom BackupAgent that reads the appropriate data during a backup operation, then create your table and insert the data during a restore operation.

Some clarity please.

If I really need to do it myself up to the SQL level, then I'm worried about the following topics:

  • Open databases and transactions. I have no idea how to close them from such a singleton class outside of my app's workflow.

  • How to notify the user that a backup is in progress and the database is locked. It might take a long time, so I might need to show a progress bar.

  • How to do the same on restore. As I understand, the restore might happen just when the user has already started using the app (and inputting data into the database). So you can't presume to just restore the backupped data in place (deleting the empty or old data). You'll have to somehow join it in, which for any non-trivial database is impossible due to the id's.

  • How to refresh the app after the restore is done without getting the user stuck at some - now - unreachable point.

  • Can I be sure the database has already been upgraded on backup or restore? Otherwise the expected schema might not match.

HitOdessit
  • 7,198
  • 4
  • 36
  • 59
pjv
  • 10,658
  • 6
  • 43
  • 60

6 Answers6

34

After revisiting my question, I was able to get it to work after looking at how ConnectBot does it. Thanks Kenny and Jeffrey!

It's actually as easy as adding:

FileBackupHelper hosts = new FileBackupHelper(this,
    "../databases/" + HostDatabase.DB_NAME);
addHelper(HostDatabase.DB_NAME, hosts);

to your BackupAgentHelper.

The point I was missing was the fact that you'd have to use a relative path with "../databases/".

Still, this is by no means a perfect solution. The docs for FileBackupHelper mention for instance: "FileBackupHelper should be used only with small configuration files, not large binary files.", the latter being the case with SQLite databases.

I'd like to get more suggestions, insights into what is expected of us (what is the proper solution), and advice on how this might break.

Community
  • 1
  • 1
pjv
  • 10,658
  • 6
  • 43
  • 60
  • 1
    I should probably add, that even though this is a bit unofficial, it is/has been working very well all this time. – pjv Dec 28 '11 at 11:57
  • 7
    Hard-coding paths is evil. – Pointer Null Mar 15 '12 at 11:22
  • @pjv, So do you make every db interaction synchronized? I'm doing the same thing and trying to figure out if I need to synchronize `db.open()` through `db.close()` or just `insert` statements or what. – NSouth Nov 25 '14 at 02:49
22

Here's yet cleaner way to backup databases as files. No hardcoded paths.

class MyBackupAgent extends BackupAgentHelper{
   private static final String DB_NAME = "my_db";

   @Override
   public void onCreate(){
      FileBackupHelper dbs = new FileBackupHelper(this, DB_NAME);
      addHelper("dbs", dbs);
   }

   @Override
   public File getFilesDir(){
      File path = getDatabasePath(DB_NAME);
      return path.getParentFile();
   }
}

Note: it overrides getFilesDir so that FileBackupHelper works in databases dir, not files dir.

Another hint: you may also use databaseList to get all your DB's and feed names from this list (without parent path) into FileBackupHelper. Then all app's DB's would be saved in backup.

Pointer Null
  • 39,597
  • 13
  • 90
  • 111
  • Combining this with the solution given by @pjv works perfectly! It may not be exactly what the specifications had in mind... but it works quite well! – Tom Apr 17 '12 at 14:37
  • 1
    That's not much use if you have databases **and** normal files to back up. – Dan Hulme Mar 21 '13 at 17:03
  • 1
    @Dan: Use multiple agents in that case. – pjv Nov 05 '13 at 17:25
  • @Pointer Null Could you elaborate a bit more one getFilesDir(), why is it really needed and why? How does it work? – powder366 Mar 27 '16 at 13:27
21

A cleaner approach would be to create a custom BackupHelper:

public class DbBackupHelper extends FileBackupHelper {

    public DbBackupHelper(Context ctx, String dbName) {
        super(ctx, ctx.getDatabasePath(dbName).getAbsolutePath());
    }
}

and then add it to BackupAgentHelper:

public void onCreate() {
    addHelper(DATABASE, new DbBackupHelper(this, DB.FILE));
}
yanchenko
  • 56,576
  • 33
  • 147
  • 165
  • 2
    @logray: That code is the exact same as in the question. Unnecessary edit. – Linuxios Feb 12 '13 at 23:48
  • @Linuxios I agree, however I think it is better to provide proper attribution to the author, rather than just blindly paste code... considering the original OP/post included a link from his app. – logray Feb 13 '13 at 00:01
  • 3
    **Important**: The (documentation)(http://developer.android.com/guide/topics/data/backup.html#Files) for backing up files states that the operation is **not threadsafe**, so you should synchronize your Database methods and your backup agents – nicopico Feb 19 '13 at 11:13
  • @yanchenko Documentation for FileBackupHelper states: "Note: This should be used only with small configuration files, not large binary files." So a database would often qualify as a large binary file... – IgorGanapolsky Jan 17 '14 at 19:55
  • This doesn't actually work! (unless I'm missing something...). The issue is that you pass in the absolute path of the db to the FileBackupHelper but the FileBackupHelper prepends the path with the files directory path eg. data/data//files which results in an incorrect path something like data/data//files/data/data//databases/ when all you want is the DB absolute name and since the super class prepends the files directory you need to make the path relative to the files directory. – bitrock Dec 15 '14 at 04:42
  • Worked for me. This link is useful for testing: http://developer.android.com/guide/topics/data/backup.html#Testing – powder366 Nov 14 '15 at 23:42
8

Using FileBackupHelper to backup/restore sqlite db raises some serious questions:
1. What happens if the app uses cursor retrieved from ContentProvider.query() and backup agent tries to override the whole file?
2. The link is a nice example of perfect (low entrophy ;) testing. You uninstall app, install it again and the backup is restored. However life can be brutal. Take a look at link. Let's imagine scenario when a user buys a new device. Since it doesn't have its own set, the backup agent uses other device's set. The app is installed and your backupHelper retrieves old file with db version schema lower than the current. SQLiteOpenHelper calls onDowngrade with the default implementation:

public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new SQLiteException("Can't downgrade database from version " +
            oldVersion + " to " + newVersion);
}

No matter what the user does he/she can't use your app on the new device.

I'd suggest using ContentResolver to get data -> serialize (without _ids) for backup and deserialize -> insert data for restore.

Note: get/insert data is done through ContentResolver thus avoiding cuncurrency issues. Serializing is done in your backupAgent. If you do your own cursor<->object mapping serializing an item can be as simple as implementing Serializable with transient field _id on the class representing your entity.

I'd also use bulk insert i.e. ContentProviderOperation example and CursorLoader.setUpdateThrottle so that the app is not stuck with restarting loader on data change during backup restore process.

If you happen do be in a situation of a downgrade, you can choose either to abort restore data or restore and update ContentResolver with fields relevant to the downgraded version.

I agree that the subject is not easy, not well explained in docs and some questions still remain like bulk data size etc.

Hope this helps.

Community
  • 1
  • 1
Piotr Bazan
  • 187
  • 2
  • 6
  • You repeat some valid questions. But I don't see how serializing the database solves the problem. It does not help with concurrency issues. And if you can't write a script to downgrade the database, then you can't write a script to deserialize old data. – pjv Nov 05 '13 at 17:28
  • @pjv If you use a ContentResolver, it solves concurrency issues for you. – IgorGanapolsky Jan 17 '14 at 20:05
  • @IgorGanapolsky Yes, I think that's true. However, if serializing is slow enough and the user already is using the app and inputting data, it may conflict with the data you are restoring. Can you do it atomically and before the user gets access to the app? – pjv Jan 19 '14 at 11:49
  • @pjv You can register a ContentObserver to synchronize the data you are observing to be notified upon. Therefore you don't have to worry about conflicts. – IgorGanapolsky Jan 20 '14 at 21:13
  • Your point about DB versions is a good one - I guess if you save your DB version (in sharedpreferences for example, assuming you also back it up) then when you are restoring you should be able to use existing logic to upgrade the DB to its current version in the onDowngrade() method. If you don't already have upgrade code then you aren't bothered with keeping data anyway! – Ben Neill Apr 28 '14 at 09:36
  • Your point about the downgrade and the user not being able to use the app is wrong. The oldVersion is stored in the file and the newVersion is what you declared in your SQLiteOpenHelper constructor. What will be called is onUpgrade(), just like if the user had upgraded the app. See around line 253 of SQLiteOpenHelper source. – sorianiv Aug 07 '15 at 22:40
  • Never, ever change your db schema without also incrementing your application's versionCode. The backup system is smart enough to avoid restoring data from a "newer" version of the app to an "older" version. If you change versionCode when you change the schema, this will automatically mean that your app is protected from newer-schema problems following restore. – ctate Dec 27 '15 at 04:32
5

As of Android M, there is now a full-data backup/restore API available to apps. This new API includes an XML-based specification in the app manifest that lets the developer describe which files to back up in a direct semantic way: 'back up the database called "mydata.db"'. This new API is much easier for developers to use -- you don't have to keep track of diffs or request a backup pass explicitly, and the XML description of which files to back up means you often don't need to write any code at all.

(You can get involved even in a full-data backup/restore operation to get a callback when the restore happens, for example. It's flexible that way.)

See the Configuring Auto Backup for Apps section on developer.android.com for a description of how to use the new API.

ctate
  • 1,379
  • 10
  • 11
  • 1
    This is only for Android 6.0 (API 23), if user has older version one still has to implement Key Value backup. – live-love Nov 29 '17 at 22:15
0

One option will be to build it in application logic above the database. It actually screams for such levell I think. Not sure if you are doing it already but most people (despite android content manager cursor approach) will introduce some ORM mapping - either custom or some orm-lite approach. And what I would rather do in this case is:

  1. to make sure your application works fine when the app/data is added in the background with new data added/removed while the application already started
  2. to make some Java->protobuf or even simply java serialization mapping and write your own BackupHelper to read the data from the stream and simply add it to database....

So in this case rather than doing it on db level do it on application level.

Jarek Potiuk
  • 19,317
  • 2
  • 60
  • 61
  • The problem is not how to get data from the database into the BackupHelperAgent or vice versa. Please read the 5 bullets at the end of my question. – pjv Jun 19 '11 at 21:41
  • @JarekPotiuk You said: "most people (despite android content manager cursor approach)". But what makes you think that most people would avoid using a ContentProvider and ContentResolver for database access? – IgorGanapolsky Jan 17 '14 at 20:08