Refactoring my backup and restore feature to comply with Scoped Storage

Gavin Wright
3 min readMay 5, 2020

--

With Google’s enforcement of scoped storage looming, I finally got around to refactoring my app’s backup/restore feature to comply with the new rules. This was a frightening proposal for me, partially because my code base is huge and partially because of the backlash surrounding these new restrictions. But, in the end, it wasn’t terribly difficult to implement.

You can see the final product in action here and here.

My backup procedure is as follows:

  1. Create a temporary directory inside of which I will assemble the backup file.
  2. In the temporary directory, create a zip file consisting of the app’s database file and SharedPreferences file.
  3. Copy the finished zip file to its final destination and give it a custom extension.

My original code was written using traditional java.io.File operations. But with the new Scoped Storage rules, these operations are being restricted to work only in the specific directories owned by my app. Luckily, my original code was already using one of these permitted directories to house the temporary directory referenced above. Specifically, I’m using the one returned by getExternalFilesDir():

This means that steps 1 and 2 of my backup procedure required no refactoring, as I could continue to use java.io.File operations in the temporary directory. However, step 3 is where changes needed to be made. In my original code, step 3 consisted of copying the finished backup file from the temporary directory over to /sdcard/MyAppName/backups/. From this location, the user would be able to more easily locate the backup file. But Scoped Storage doesn’t grant me write access in this directory, so I would get a FileNotFoundException when trying to write to the directory returned by the following code:

This no longer works with Scoped Storage

So this is where the Storage Access Framework (SAF) comes in. By using the built-in SAF picker, we can prompt the user to choose a save directory for the backup file. The picker will create a file in this directory and give us back a URI pointing to the file. With this URI, we can then modify the underlying file. The steps to do this are outlined below.

Before anything, let’s define some constants that will be used throughout:

Define constants

Now we construct and launch an intent for creating the backup file:

Launch the SAF picker in ACTION_CREATE_DOCUMENT mode to create a new file
Prompt user to choose a save directory and file name for the backup file

Next, we get the result URI and start the backup procedure:

Get URI reference to the the file created by the SAF picker

Now we perform the actual backup procedure. The various helper methods are included as well:

Method for performing backup procedure
Helper method which backs up my app’s database file
Helper method which copies a file using traditional file IO
Helper method which backs up my app’s SharedPreferences file
Helper method which creates a zip file from a list of files
Helper method which copies a file using Input/Output Streams, thus making it compatible with URIs
Helper method for displaying an error on the UI thread

And that’s what it takes to create a backup!

Next, we’ll look at the procedure for restoring the app data from an existing backup. This is largely the reverse of what’s shown above.

First, we create and launch an intent for selecting a backup file from the SAF picker:

Launch SAF picker in ACTION_OPEN_DOCUMENT mode for selecting a file to open
Prompt user to select a backup file to be restored

Now we get the result URI and start the restore procedure:

Get URI reference to the file selected in the SAF picker

Then we perform the actual restore procedure. Any helper methods that weren’t previously listed will be shown below as well:

Method for performing restore procedure
Helper method for restoring the database file from the given file
Helper method for restoring the SharedPreferences file from the given file

And that’s it for the restore procedure! The only major addition in order to facilitate Scoped Storage was the copyFileUsingStreams() method. Note that I omitted some UI code, such as a ProgressBar that displays while a backup or restore is taking place.

One particular pain point had to do with supporting files stored on or retrieved from Google Drive. It turns out that zip files stored locally on a device will maintain the assigned mimetype of application/octet-stream, while files uploaded to Google Drive will automatically have their mimetypes changed to application/x-zip. Once I learned this, it was then possible to support both storage locations simultaneously.

I hope this helps!

--

--