たくさんの自由帳

家の回線調子わるい

本題

Androidのデータベース、Roomのバックアップを取りたい
他のスマホにデータベースを移したい

データベースのファイル どこ?

data/data/${packageName}/databasesにある。
なおこんな面倒なことしなくても、Context#databaseList()を使うとこのフォルダに入ってるファイルの名前を配列で返してくれるし、
これを使ってContext#getDatabasePath()を使えばパスが取れる。

なお、data/dataroot権限がないと見れないが、開発中のアプリはAndroid StudioのDevice File Explorerから見ることができる。

どれがデータベースのファイル?

.db.db-shm.db-walをコピーすれば多分見れる。
.dbファイル一個だけにしたい場合は、Room.databaseBuilderのときにsetJournalMode(RoomDatabase.JournalMode.TRUNCATE)を呼べばもしかすると.db一個だけになるかもしれない。

nicoHistoryDB = Room.databaseBuilder(context, NicoHistoryDB::class.java, "NicoHistory.db")
    .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) // これ
    .build()

バックアップ

今回はContext#getExternalFileDir()の場所に保存したいと思います。
なおAndroid 10まではファイルマネージャーで見ることができますが、Android 11ではサードパーティファイルマネージャーでは見れません。
Android標準のファイルマネージャー(com.google.android.documentui)を開ければ見れると思います(Pixel以外はしらん)

// アプリ固有ストレージに逃がす
context.databaseList()
    .map { name -> context.getDatabasePath(name) }
    .forEach { dbFile ->
        // アプリ固有ストレージ(外部)に保存する
        File(context.getExternalFilesDir(null), dbFile.name).let { file ->
            // ファイル作成
            file.createNewFile()
            // データ書き込み
            file.writeBytes(dbFile.readBytes())
        }
    }

sdcard/Android/data/パッケージ名/filesにあると思います。
なんか思ったよりきれいに書きことができた

リストア

コピーのときと同じように、Context#getExternalFilesDir()にデータベースのデータが有るとして、
そいつらをdata/data/パッケージ名/databasesに移せばおk

// データベースのパス。Context#getDataDir()はAndroid 7以降のみなのでCompatを使う
val dbFolder = File(ContextCompat.getDataDir(context), "databases")
// アプリ固有ストレージ(外部)のファイルを取り出す
context.getExternalFilesDir(null)?.listFiles()?.forEach { dbFile ->
    File(dbFolder, dbFile.name).let { file ->
        // ファイル作成
        file.createNewFile()
        // データ書き込み
        file.writeBytes(dbFile.readBytes())
    }
}

ファイルピッカーで選ばせたほうがいいよね

Storage Access Frameworkでユーザーにファイルを選ばせる(作成、復元)ほうがいいと思います。
ただ複数のファイルを選ぶ機能はSAFには(多分)無いので、データベースのファイルをzipファイルにまとめて扱うことになりそう(他にもあるかも)

というわけで例です:

Activity Result APIを使うので、app/build.gradleに数行足してください。

dependencies {
    // Activity Result APIでonActivityResultを駆逐する。
    implementation 'androidx.activity:activity-ktx:1.3.0-alpha02'
    implementation 'androidx.fragment:fragment-ktx:1.3.0'

}

バックアップする

/** データベースをバックアップする */
class BackupActivity : AppCompatActivity() {

    /** Activity Result API を使う。[AppCompatActivity.onActivityResult]の後継 */
    private val callback = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
        if (uri != null) {
            // ZIP作成。
            val outputStream = contentResolver.openOutputStream(uri)
            ZipOutputStream(outputStream).let { zip ->
                /**
                 * データベースフォルダをすべてZipに入れる
                 * */
                databaseList()
                    .map { name -> getDatabasePath(name) }
                    // よくわからんファイルは持ってこない。
                    .filter { file -> !file.name.contains("com.google.android.datatransport") }
                    .forEach { file ->
                        val inputStream = file.inputStream() // ファイル読み出し
                        val entry = ZipEntry(file.name) // ファイル名
                        zip.putNextEntry(entry)
                        zip.write(inputStream.readBytes()) // 書き込む。Kotlinかんたんすぎい
                        inputStream.close()
                        zip.closeEntry()
                    }
                // おしまい
                zip.close()
                // 終了
                Toast.makeText(this, "バックアップできた", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_backup)

        // Storage Access Framework開始
        callback.launch("backup.zip")

    }
}

リストアする

/** リストアするActivity */
class RestoreActivity : AppCompatActivity() {

    /** Activity Result API を使う。[AppCompatActivity.onActivityResult]の後継 */
    private val callback = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
        if (uri != null) {
            // データベースが保存されているフォルダのパス。ContextCompatで後方互換性もはいばっちり!
            val databaseFolder = File(ContextCompat.getDataDir(this), "databases")
            // 一応前のデータを消す
            databaseFolder.listFiles()?.forEach { file -> file.delete() }
            // Zip展開
            val inputStream = contentResolver.openInputStream(uri)
            ZipInputStream(inputStream).let { zip ->
                var zipEntry: ZipEntry?
                // Zip内のファイルをなくなるまで繰り返す
                while (zip.nextEntry.also { zipEntry = it } != null) {
                    if (zipEntry != null) {
                        // コピー先ファイル作成
                        val dbFile = File(databaseFolder, zipEntry!!.name)
                        dbFile.createNewFile()
                        // データを書き込む
                        dbFile.writeBytes(zip.readBytes())
                    }
                }
            }
            // おわった
            Toast.makeText(this, "リストアおわった", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_restore)

        // Storage Access Framework
        callback.launch(arrayOf("*/*")) // application/zip 動かないのかな

    }
}

以上です。

おわりに

Caused by: java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode

これは、app/build.gradleimplementation 'androidx.fragment:fragment-ktx:1.3.0'を付け足すと直ります?
Activityしか使わんしって思ったんですけどだめっぽいです。