Refactoring my backup and restore feature to comply with Scoped Storage
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.
My backup procedure is as follows:
- Create a temporary directory inside of which I will assemble the backup file.
- In the temporary directory, create a zip file consisting of the app’s database file and
SharedPreferences
file. - 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:
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:
Now we construct and launch an intent for creating the backup file:
Next, we get the result URI and start the backup procedure:
Now we perform the actual backup procedure. The various helper methods are included as well:
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:
Now we get the result URI and start the restore procedure:
Then we perform the actual restore procedure. Any helper methods that weren’t previously listed will be shown below as well:
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!