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:
bmgr fullbackup refuses to run and Logcat says “dataset not yet initialized”. Run bmgr backupnow instead, which sets things up first.bmgr init to fix that message, because it wipes the backup storage and the message comes straight back.-wal file too, so you don’t need to list it.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:
adb shell bmgr transport com.android.localtransport/.LocalTransport switches to the local transport.adb shell bmgr backupnow <your.app.id> backs up an app straight away.adb shell bmgr fullbackup <your.app.id> also forces a backup, though not quite the same way, as I found out.adb shell bmgr init <transport> resets a transport, which comes up again further down.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.
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:
receiptally.db is the database itself, although it doesn’t always hold the newest data.receiptally.db-wal is the write-ahead log, where new writes land first before SQLite copies them into the main file in a step it calls a checkpoint.receiptally.db-shm is a small index that helps SQLite find things in the WAL quickly. SQLite’s file format notes say it “does not contain any database content and is not required to recover the database following a crash.”The SQLite WAL page explains the arrangement in more depth.
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:
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.
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.
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.”
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.
Looking back, the quickest checks were already in the tools:
bmgr backupnow prints a result for each app, so a failed backup tells you so in the terminal without any digging through Logcat.adb shell run-as <your.app.id> ls -l files/ databases/ shows what actually came back before you even open the app.