A Basic Backup Example
With all that as prologue, let’s look at the backup and restore logic in the ToDo/MVI
sample project, originally covered in a chapter on the Model-View-Intent architecture pattern.
Triggering the Operation
The RosterListFragment
has a pair of action bar items for “Backup” and “Restore”. These are tied to backup()
and restore()
methods, which in turn pass control to launchAndGoAway()
:
private void backup() {
launchAndGoAway(true);
}
private void restore() {
launchAndGoAway(false);
}
private void launchAndGoAway(boolean isBackup) {
ToDoDatabase.shutdown();
startActivity(BackupRestoreActivity.newIntent(getActivity(), isBackup));
}
launchAndGoAway()
starts by calling a shutdown()
method on ToDoDatabase
, which simply calls close()
on our singleton (if it exists) and sets that singleton field to null
:
public synchronized static void shutdown() {
if (INSTANCE!=null) {
INSTANCE.close();
INSTANCE=null;
}
}
In principle, this resets matters to where we were before we started using the database. Right now, close()
does not appear to perform any disk I/O; if some future version of Room does perform I/O here, we would want to make our shutdown()
call be asynchronous (perhaps using an RxJava Completable
).
In addition to ToDoDatabase.shutdown()
, this would be the spot where our code would need to suspend any other background work, particularly work that might be triggered even if our process goes away. This includes things like WorkManager
, JobScheduler
, and AlarmManager
. In this case, the app has none of these things, so there is nothing that we need to worry about.
Finally, launchAndGoAway()
retrieves an Intent
from BackupRestoreActivity
, then starts an activity based on that Intent
. BackupRestoreActivity
is where our actual backup and restore logic resides. That newIntent()
method builds an Intent
identifying BackupRestoreActivity
and populates that Intent
as BackupRestoreActivity
needs:
static Intent newIntent(Context ctxt, boolean isBackup) {
return new Intent(ctxt, BackupRestoreActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_IS_BACKUP, isBackup)
.putExtra(EXTRA_MAIN_PID, Process.myPid());
}
Specifically, we:
- Use
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK)
to destroy any outstanding activities (in this case, it would be justMainActivity
) - Add the supplied boolean value as an extra, to denote whether this is a backup or a restore request
- Capture our PID and put that as an extra as well
Our UI
BackupRestoreActivity
uses a dialog theme (Theme.Apptheme.Dialog
), so it does not take up the entire screen:
<style name="Theme.Apptheme.Dialog" parent="@android:style/Theme.Material.Light.Dialog.NoActionBar">
<item name="android:colorPrimary">@color/primary</item>
<item name="android:colorPrimaryDark">@color/primary_dark</item>
<item name="android:colorAccent">@color/accent</item>
<item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
<item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
</style>
Our layout (activity_progress
) is just a ProgressBar
and TextView
, wrapped in a ConstraintLayout
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/msg_wait"
android:textAppearance="@android:style/TextAppearance.Material.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/progressBar"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
The net result is that we will show indefinite progress as our UI:
In onCreate()
, we load up that layout, plus we get an instance of our viewmodel (VM
) via a VMFactory
:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_progress);
VMFactory factory=
new VMFactory(getApplication(),
getIntent().getBooleanExtra(EXTRA_IS_BACKUP, true),
getIntent().getIntExtra(EXTRA_MAIN_PID, -1));
VM vm=ViewModelProviders.of(this, factory).get(VM.class);
vm.results.observe(this, unused -> {
startActivity(MainActivity.newIntent(this));
finish();
});
}
That VMFactory
takes our extras and passes them into the viewmodel:
private class VMFactory extends ViewModelProvider.AndroidViewModelFactory {
private final boolean isBackup;
private final int pid;
public VMFactory(@NonNull Application app, boolean isBackup, int pid) {
super(app);
this.isBackup=isBackup;
this.pid=pid;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T)new VM(getApplication(), isBackup, pid);
}
}
The VM
viewmodel exposes a LiveData
named results
, which will emit an object when our backup or restore is completed. At that point, we start up MainActivity
again and finish()
the BackupRestoreActivity
, so the user is taken right back to the main app UI.
Performing the Operation
The “heavy lifting” for the backup and restore operations is handled inside of VM
for simplicity. One could argue that this sort of work belongs in a repository, and for mainstream functionality or a larger app that may make sense. However, the only place where such a repository would be used is right here in this viewmodel, so it is unclear how this book example would be improved by using a repository.
In onCreate()
, we set up an RxJava Single
that invokes a process()
method that will do the real work. The Single
just allows us to do that work on a background thread, plus get a LiveData
for our UI layer to use (via LiveDataReactiveStreams.fromPublisher()
):
public static class VM extends AndroidViewModel {
final LiveData<Boolean> results;
public VM(@NonNull Application application, boolean isBackup, int pid) {
super(application);
Single<Boolean> backup=
Single.create((SingleOnSubscribe<Boolean>)emitter -> {
process(isBackup, pid);
emitter.onSuccess(true);
})
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread());
results=LiveDataReactiveStreams.fromPublisher(backup.toFlowable());
}
process()
starts off by sleeping for one second, to give that main process extra time for any cleanup, particularly for any asynchronous work that we do not control. Then, it uses Process.killProcess()
to terminate the main process, using the PID passed in via the extra:
private void process(boolean isBackup, int pid) throws IOException {
SystemClock.sleep(1000); // wait for things to settle
Process.killProcess(pid);
File dbDir=getApplication().getDatabasePath("foo").getParentFile();
File extDir=getApplication().getExternalFilesDir(null);
File backupDir=new File(extDir, "db-backup");
if (isBackup) {
if (backupDir.exists()) {
delete(backupDir);
}
backupDir.mkdirs();
copy(dbDir, backupDir);
}
else {
if (dbDir.exists()) {
delete(dbDir);
}
dbDir.mkdirs();
copy(backupDir, dbDir);
}
}
}
What happens next depends a bit on the operation:
- If we are performing a backup, we make an empty directory for that backup, then copy the files from the database directory to the backup directory
- If we are performing a restore, we make an empty directory for the database, then copy the files from the backup directory to the database directory
You can see all of this in action by running the sample app:
- Create a couple of to-do items
- Run a backup
- Change your roster of to-do items (e.g., add some more)
- Run a restore
- See that your backed-up roster of to-do is restored
Areas for Improvement
There are many things that could be improved in this sample, things that a production app would need to have:
- We are doing no validation to confirm that the backed up data is a complete bit-for-bit copy of the original data. Probably it is, but random I/O hiccups could give us a corrupt backup, which is useless.
- We are deleting the “real” database prior to restoring the backup. If the restore process fails for some reason, we are now stuck. We could, instead, rename the database directory, restore to a new database directory, and confirm the restore result to ensure that it is a bit-for-bit copy of the backup and we can successfully open the database. If that succeeds, we can then delete the renamed older database directory. If the restore fails, we can delete the failed restored copy, rename the old database directory back to its original name, and be right back where we started prior to the restoration attempt.
- We are only allowing one backup, in a location chosen by us as developers. We could use
ACTION_OPEN_DOCUMENT_TREE
to allow the user to choose where to make the backup. In this case, though, we cannot be completely certain that the backup location will be on the device, as we get aUri
to a documents provider. It is possible that working with that provider will be slow, particularly if it is transferring the data in real time to a server. It may be better to make a local backup first, then copy the backup to the provider. - We are not validating the backup. We assume that it exists, and that it contains a valid database. Clearly, this might not be the case.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.