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:

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:

RoomBackup UI
RoomBackup 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:

You can see all of this in action by running the sample app:

Areas for Improvement

There are many things that could be improved in this sample, things that a production app would need to have:


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.