Debugging an Android Auto Backup Restore That Came Back Empty

- 13 mins

I added Android Auto Backup to Receiptally, my receipt-scanning app, so that receipts would survive an uninstall or a move to a new phone. I’d also hoped it could be the start of syncing receipts between a user’s devices through their own Google Drive, without Firebase or a server of my own.

The short version, if you’re testing Auto Backup:

First, a word on bmgr and adb

I’d never used bmgr before this, so in case you haven’t either: it’s a command-line tool on the phone or emulator that lets you drive Android’s Backup Manager by hand, so you can test backup and restore without waiting for Android to do it on its own schedule (bmgr docs). You reach it from your computer through adb, the Android Debug Bridge, which comes with the Android SDK Platform Tools.

There’s one more piece of jargon you’ll need: Android sends backups through a transport, which decides where the backup data ends up. On a normal phone that’s Google’s, which uploads it to your Google Drive. There’s also a local transport that keeps the backup on the device itself, which is much quicker for testing.

The commands that matter here are:

Android’s testing guide has a small script that does the whole backup, uninstall and restore round trip, and in hindsight that’s where I should have started.

How I set up the backup rules

Auto Backup decides what to back up from an XML rules file that your AndroidManifest.xml points to (the Auto Backup docs explain the format). Receiptally keeps each receipt’s image in files/receipts/ and its extracted data in a Room database, so my rules included both:

<data-extraction-rules>
    <cloud-backup>
        <include domain="file" path="receipts/"/>
        <include domain="database" path="receiptally.db"/>
    </cloud-backup>
</data-extraction-rules>

A Room database is usually more than one file on disk. Room runs SQLite in write-ahead log (WAL) mode, so receiptally.db normally has two companions next to it:

The SQLite WAL page explains the arrangement in more depth.

What went wrong and what I tried

I left the WAL out of my rules on purpose. Auto Backup copies files one at a time, and I worried that backing up both could capture the database and its WAL out of step with each other.

I switched to the local transport, ran adb shell bmgr backupnow app.receiptally and adb shell bmgr fullbackup app.receiptally a few times, uninstalled, reinstalled and opened the app to an empty list. The receipts folder was gone and receiptally.db was only 4 KB. Logcat, Android’s system log, had these lines from the backup attempts:

I BackupManagerService: Full backup requested but dataset not yet initialized
I BackupManagerService: Full backup not currently possible -- key/value backup not yet run?
D BackupManagerService: Done with full transport backup.

“Dataset not yet initialized” sounded like the local transport needed setting up, so I ran adb shell bmgr init com.android.localtransport/.LocalTransport. The 4 KB database also made me think the receipts were stuck in the WAL I’d left out, so I added receiptally.db-wal to the rules after all.

I ran the test again and everything came back, but because both fixes went in together I never found out which one had mattered.

Before writing this up I wanted to know, so I retested on a fresh Android 14 emulator with neither fix: the original rules, and no init. Scanning real receipts on an emulator is fiddly, so I wrote a small piece of test-only code for the debug build that saves three fake receipts through the app’s normal database code, and triggered it from the terminal:

See the test-only code that saves fake receipts

It’s a broadcast receiver in the app’s debug source set (app/src/debug/), so it never ships in a release build. It uses Hilt to inject the DAO; if your app doesn’t use Hilt, get the DAO however the rest of your app does. ReceiptDao and ReceiptEntity are Receiptally’s own Room classes, so swap in your own DAO, entity and insert method.

// app/src/debug/java/app/receiptally/debug/SeedReceiver.kt
package app.receiptally.debug

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import app.receiptally.core.data.room.dao.ReceiptDao
import app.receiptally.core.data.room.entity.ReceiptEntity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.util.UUID
import javax.inject.Inject

@AndroidEntryPoint
class SeedReceiver : BroadcastReceiver() {

    @Inject lateinit var receiptDao: ReceiptDao

    override fun onReceive(context: Context, intent: Intent) {
        val count = intent.getIntExtra("count", 3)
        val pendingResult = goAsync() // keeps the broadcast alive until the coroutine finishes

        CoroutineScope(Dispatchers.IO).launch {
            try {
                val receiptsDir = File(context.filesDir, "receipts").apply { mkdirs() }
                repeat(count) { i ->
                    val id = UUID.randomUUID().toString()
                    // A fake image file, so the receipts folder has something to back up.
                    val image = File(receiptsDir, "$id.jpg").apply { writeBytes(ByteArray(400_000)) }
                    receiptDao.upsert(fakeReceipt(id, image, "Seeded Merchant ${i + 1}"), emptyList())
                }
                Log.i("SeedReceiver", "Seeded $count receipts")
            } finally {
                pendingResult.finish()
            }
        }
    }

    private fun fakeReceipt(id: String, image: File, merchant: String) = ReceiptEntity(
        id = id,
        imageUri = Uri.fromFile(image).toString(),
        merchantName = merchant,
        totalAmount = "12.34", totalCurrency = "GBP",
        subtotalAmount = null, subtotalCurrency = null,
        taxAmount = null, taxCurrency = null,
        purchaseDate = "2026-05-08", purchaseTime = null,
        rawExtractedText = "seed",
        reviewStatus = "NeedsReview",
        createdAt = 0, updatedAt = 0,
    )
}

Register it in the debug manifest, app/src/debug/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <receiver android:name="app.receiptally.debug.SeedReceiver" android:exported="true">
            <intent-filter>
                <action android:name="app.receiptally.debug.SEED" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

Open the app once so the database exists, then trigger the receiver from the terminal. adb logcat -s SeedReceiver shows Seeded 3 receipts when it’s done.

adb shell am broadcast -n app.receiptally/.debug.SeedReceiver --ei count 3

With the fake receipts saved, I backed up with adb shell bmgr backupnow app.receiptally only, without fullbackup this time, and it printed this (minus a long run of progress lines):

Running incremental backup for 1 requested packages.
Package @pm@ with result: Success
Package app.receiptally with result: Success
Backup finished with result: Success

After an uninstall and reinstall, all three receipts came back along with their images, so neither fix had been needed.

What that log message actually meant

So why did my first attempt print those “dataset not yet initialized” lines when the emulator run didn’t? They come from bmgr fullbackup, which I’d run alongside backupnow the first time and skipped on the emulator. fullbackup refuses to run until the device has done at least one ordinary backup, and on a fresh device nothing has.

backupnow does that first backup on its own, and the @pm@ line in its output above is Android recording some details about the installed apps before it backs up yours. On the emulator, bmgr fullbackup run before any other backup printed the same three lines, and after a backupnow it worked.

As for bmgr init, rather than setting anything up, it wipes everything stored in the local transport, including the record of that first backup. On the emulator it logged Initializing (wiping) backup transport storage, and fullbackup failed again straight after it. My fix most likely only looked like one because the backupnow I ran next did the real setup.

Android’s testing guide mentions the second of those log lines and gives the proper fix: “Trigger a key-value backup with the command bmgr run, and then try again.” Key/value backup is Android’s other kind of backup, and running one does the same first step that backupnow does.

My best guess for the first failure is that my fullbackup runs came before any backup had completed, so nothing was backed up before I uninstalled. That would explain why the images didn’t come back either, though I can’t prove it now.

Why the WAL file didn’t need adding

My hunch about the WAL was partly right, because that’s where the receipts were. With the three fake receipts saved, listing the database folder showed this:

$ adb shell run-as app.receiptally ls -l databases/
-rw-rw---- 1 u0_a192 u0_a192  4096 ... receiptally.db
-rw------- 1 u0_a192 u0_a192 32768 ... receiptally.db-shm
-rw------- 1 u0_a192 u0_a192 90672 ... receiptally.db-wal

(run-as runs a command as your app, which is how you can see its private files on a debug build.)

The main file is a single 4 KB page with no receipts in it yet, and all three are sitting in the 90 KB WAL, so a backup without the WAL would have restored an empty database.

But the backup’s Logcat output on the emulator lists the WAL even though my original rules only named receiptally.db:

I FullBackup_native: measured [/data/data/app.receiptally/databases/receiptally.db-wal] at 91648

So Android adds the WAL for you when you include the database, and the line I’d added did nothing. It leaves the -shm file out, which is fine because that file holds no data.

That also settles my original worry, because leaving the WAL out really would have been the risky choice. The SQLite docs warn that “If a database file is separated from its WAL file, then transactions that were previously committed to the database might be lost, or the database file might become corrupted.” And copying the two files out of step isn’t a problem here, because Android’s Auto Backup docs say that “During Auto Backup, the system shuts down the app to make sure it is no longer writing to the file system.”

What about syncing between devices?

I’d hoped this could grow into syncing receipts between a user’s devices, but Auto Backup isn’t built for that. Its docs say data “is restored whenever the app is installed”, and that “Only the most recent backup is stored.” So a second phone gets the receipts once, when the app is installed on it, and never hears about new receipts after that (I’ve only tested restores on the same device so far). Auto Backup is also Android only, and Receiptally has an iPhone app too.

For syncing, Google Drive’s application data folder looks like a better fit. Google describes it as “a special hidden folder that your app can use to store application-specific data”, and an app reads and writes it through the Drive API. That’s a separate piece of work I’m still looking into.

How I’d debug this next time

Looking back, the quickest checks were already in the tools:

Ugo

Ugo

Full stack polyglot software developer