Key/Value Backupにハマったので知見を残しておく

#前述

Androidでは、再インストールや機種変でアプリケーションを入れ直したとき前回使用時の状態に復元するバックアップ機能が用意されています。
詳しくはこちらの[公式ドキュメント][1]をご覧ください。

バックアップ機能には、次の2種類が用意されており、今回は、Key/Value Backupにのみ焦点をおいてまとめます。

* Android Auto Backup：Android 6.0以上から使えます
* Key/Value Backup：Android 2.2以上から使えます

#バックアップに関連するadbコマンド

#####`adb shell bmgr list transports`

データのバックアップ先の一覧を確認することができます。
端末によって一覧の数も異なり、一覧が表示されない場合はバックアップ機能が提供されていない端末となるようです。

```
$ adb shell bmgr list transports
    android/com.android.internal.backup.LocalTransport
    com.google.android.gms/.backup.migrate.service.D2dTransport
  * com.google.android.gms/.backup.BackupTransportService
```

* android/com.android.internal.backup.LocalTransport
<br>
Googleのクラウド上ではなく端末のローカルにバックアップされます
* com.google.android.gms/.backup.migrate.service.D2dTransport
<br>
不明
* com.google.android.gms/.backup.BackupTransportService
<br>
Googleアカウントに紐付き、クラウド上にバックアップされます

#####`adb shell bmgr transport <任意のtransport>`

バックアップ先を切り替えることができます。
adb shell bmgr list transportsで出力されたものの中から選択することができます。

adbコマンド以外からは切り替える方法がないため、開発中のテストで使用するものだと考えられます。
私が利用したケースとして、Android 5.1.1の端末においてバックアップテストを繰り返していると、復元をしても最新のバックアップデータで復元されない問題が発生しました。Logcatを確認していると、rate limiterというログが表示されバックアップ制限に引っかかっていることがわかりました。その際にバックアップ先をLocalTransportに変更してテストを行いました。そのときに参考になったstack overflowが[こちら][2]。

```
$ adb shell bmgr transport android/com.android.internal.backup.LocalTransport
Selected transport android/com.android.internal.backup.LocalTransport (formerly com.google.android.gms/.backup.BackupTransportService)
```

#####`adb shell dumpsys backup`

各アプリケーションのバックアップ状態を確認することができます。
ここで主に確認する点は下記の2点です。

* Ever backed up: XXX
<br>
バックアップされているアプリケーションが表示されます
* Pending key/value backup: XXX
<br>
バックアップ待ちのアプリケーションが表示されます
EverとPendingの両方にある場合は、まだ最新のバックアップデータに上書きされていないことになります

```
$ adb shell dumpsys backup
Backup Manager is enabled / provisioned / not pending init
Auto-restore is enabled
Backup currently running
Last backup pass started: 1547303056670 (now = 1547303060329)
  next scheduled: 1547317741697
Transport whitelist:
    android/com.android.internal.backup.LocalTransportService
    com.google.android.gms/.backup.component.D2dTransportService
    com.google.android.gms/.backup.BackupTransportService
Available transports:
  * android/com.android.internal.backup.LocalTransport
       destination: Backing up to debug-only private cache
       intent: null
       @pm@ - 2060 state bytes
       com.google.android.apps.maps - 124 state bytes
       android - 477 state bytes
       com.google.android.apps.messaging - 124 state bytes
       com.android.cellbroadcastreceiver - 116 state bytes
       com.google.android.googlequicksearchbox - 116 state bytes
       com.google.android.videos - 72 state bytes
       com.android.providers.settings - 84 state bytes
       com.android.calllogbackup - 0 state bytes
       com.android.vending - 0 state bytes
       com.google.android.deskclock - 0 state bytes
       com.android.chrome - 1305 state bytes
       com.google.android.apps.photos - 104 state bytes
       com.google.android.apps.wallpaper - 124 state bytes
       com.android.providers.blockednumber - 4 state bytes
       com.google.android.dialer - 124 state bytes
       com.android.documentsui - 112 state bytes
    com.google.android.gms/.backup.migrate.service.D2dTransport
       destination: Moving data to new device
       intent: null
    com.google.android.gms/.backup.BackupTransportService
       destination: Add a backup account now
       intent: Intent { cmp=com.google.android.gms/.backup.SetBackupAccountActivity }
Pending init: 0
Most recent backup trace:
   beginBackup: [ com.google.android.apps.maps android com.google.android.apps.messaging com.android.cellbroadcastreceiver com.google.android.googlequicksearchbox com.google.android.videos com.android.providers.settings com.android.calllogbackup com.google.android.apps.inputmethod.hindi com.android.vending com.google.android.deskclock com.android.chrome com.google.android.apps.photos com.google.android.apps.wallpaper com.google.android.inputmethod.pinyin com.android.providers.blockednumber com.google.android.dialer com.android.sharedstoragebackup com.android.documentsui com.google.android.gm com.google.android.apps.docs com.google.android.inputmethod.latin com.google.android.youtube com.google.android.music com.android.providers.userdictionary @pm@ com.google.android.calendar ]
   initializing transport com.android.internal.backup.LocalTransport
   transport.initializeDevice() == 0
   invoking @pm@
   setting timeout
   calling agent doBackup()
   invoke success
   PMBA invoke: 0
   exiting prelim: 0
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=26
   launch agent for com.google.android.apps.maps
   agent bound; a? = true
   invoking com.google.android.apps.maps
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.apps.maps
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=25
   launch agent for android
   agent bound; a? = true
   invoking android
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding android
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=24
   launch agent for com.google.android.apps.messaging
   agent bound; a? = true
   invoking com.google.android.apps.messaging
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.apps.messaging
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=23
   launch agent for com.android.cellbroadcastreceiver
   agent bound; a? = true
   invoking com.android.cellbroadcastreceiver
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.cellbroadcastreceiver
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=22
   launch agent for com.google.android.googlequicksearchbox
   agent bound; a? = true
   invoking com.google.android.googlequicksearchbox
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.googlequicksearchbox
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=21
   launch agent for com.google.android.videos
   agent bound; a? = true
   invoking com.google.android.videos
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.videos
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=20
   launch agent for com.android.providers.settings
   agent bound; a? = true
   invoking com.android.providers.settings
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.providers.settings
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=19
   launch agent for com.android.calllogbackup
   agent bound; a? = true
   invoking com.android.calllogbackup
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.calllogbackup
   operation complete
   no data to send
   executeNextState => RUNNING_QUEUE
   invoke q=18
   launch agent for com.google.android.apps.inputmethod.hindi
   skipping - not eligible, completion is noop
   executeNextState => RUNNING_QUEUE
   expecting completion/timeout callback
   invoke q=17
   launch agent for com.android.vending
   agent bound; a? = true
   invoking com.android.vending
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.vending
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=16
   launch agent for com.google.android.deskclock
   agent bound; a? = true
   invoking com.google.android.deskclock
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.deskclock
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=15
   launch agent for com.android.chrome
   agent bound; a? = true
   invoking com.android.chrome
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.chrome
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=14
   launch agent for com.google.android.apps.photos
   agent bound; a? = true
   invoking com.google.android.apps.photos
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.apps.photos
   operation complete
   no data to send
   executeNextState => RUNNING_QUEUE
   invoke q=13
   launch agent for com.google.android.apps.wallpaper
   agent bound; a? = true
   invoking com.google.android.apps.wallpaper
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.apps.wallpaper
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=12
   launch agent for com.google.android.inputmethod.pinyin
   skipping - not eligible, completion is noop
   executeNextState => RUNNING_QUEUE
   expecting completion/timeout callback
   invoke q=11
   launch agent for com.android.providers.blockednumber
   agent bound; a? = true
   invoking com.android.providers.blockednumber
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.providers.blockednumber
   operation complete
   no data to send
   executeNextState => RUNNING_QUEUE
   invoke q=10
   launch agent for com.google.android.dialer
   agent bound; a? = true
   invoking com.google.android.dialer
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.google.android.dialer
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=9
   launch agent for com.android.sharedstoragebackup
   skipping - not eligible, completion is noop
   executeNextState => RUNNING_QUEUE
   expecting completion/timeout callback
   invoke q=8
   launch agent for com.android.documentsui
   agent bound; a? = true
   invoking com.android.documentsui
   setting timeout
   calling agent doBackup()
   invoke success
   expecting completion/timeout callback
   unbinding com.android.documentsui
   operation complete
   sending data to transport
   data delivered: 0
   finishing op on transport
   finished: 0
   executeNextState => RUNNING_QUEUE
   invoke q=7
   launch agent for com.google.android.gm
Ancestral: 0
Current:   0
Participants:
  uid: 1000
    com.android.providers.settings
    android
  uid: 10002
    com.android.providers.blockednumber
    com.android.calllogbackup
    com.android.providers.userdictionary
  uid: 10008
    com.android.cellbroadcastreceiver
  uid: 10010
    com.android.documentsui
  uid: 10020
    com.google.android.dialer
  uid: 10024
    com.android.vending
  uid: 10026
    com.android.sharedstoragebackup
  uid: 10031
    com.google.android.apps.wallpaper
  uid: 10032
    com.google.android.googlequicksearchbox
  uid: 10043
    com.android.chrome
  uid: 10046
    com.google.android.calendar
  uid: 10050
    com.google.android.apps.docs
  uid: 10054
    com.google.android.apps.inputmethod.hindi
  uid: 10056
    com.google.android.inputmethod.pinyin
  uid: 10059
    com.google.android.inputmethod.latin
  uid: 10062
    com.google.android.music
  uid: 10063
    com.google.android.apps.maps
  uid: 10067
    com.google.android.apps.photos
  uid: 10068
    com.google.android.deskclock
  uid: 10069
    com.google.android.apps.messaging
  uid: 10072
    com.google.android.gm
  uid: 10074
    com.google.android.videos
  uid: 10079
    com.google.android.youtube
Ancestral packages: none
Ever backed up: 16
    com.google.android.apps.maps
    android
    com.android.vending
    com.google.android.deskclock
    com.google.android.apps.messaging
    com.android.cellbroadcastreceiver
    com.android.chrome
    com.google.android.googlequicksearchbox
    com.google.android.videos
    com.google.android.apps.photos
    com.google.android.apps.wallpaper
    com.android.providers.blockednumber
    com.google.android.dialer
    com.android.providers.settings
    com.android.documentsui
    com.android.calllogbackup
Pending key/value backup: 26
    BackupRequest{pkg=com.google.android.apps.maps}
    BackupRequest{pkg=android}
    BackupRequest{pkg=com.google.android.apps.messaging}
    BackupRequest{pkg=com.android.cellbroadcastreceiver}
    BackupRequest{pkg=com.google.android.googlequicksearchbox}
    BackupRequest{pkg=com.google.android.videos}
    BackupRequest{pkg=com.android.providers.settings}
    BackupRequest{pkg=com.android.calllogbackup}
    BackupRequest{pkg=com.google.android.apps.inputmethod.hindi}
    BackupRequest{pkg=com.android.vending}
    BackupRequest{pkg=com.google.android.deskclock}
    BackupRequest{pkg=com.android.chrome}
    BackupRequest{pkg=com.google.android.apps.wallpaper}
    BackupRequest{pkg=com.google.android.apps.photos}
    BackupRequest{pkg=com.google.android.inputmethod.pinyin}
    BackupRequest{pkg=com.android.providers.blockednumber}
    BackupRequest{pkg=com.google.android.dialer}
    BackupRequest{pkg=com.android.sharedstoragebackup}
    BackupRequest{pkg=com.android.documentsui}
    BackupRequest{pkg=com.google.android.gm}
    BackupRequest{pkg=com.google.android.apps.docs}
    BackupRequest{pkg=com.google.android.inputmethod.latin}
    BackupRequest{pkg=com.google.android.music}
    BackupRequest{pkg=com.google.android.youtube}
    BackupRequest{pkg=com.android.providers.userdictionary}
    BackupRequest{pkg=com.google.android.calendar}
Full backup queue:38
    0 : com.android.cts.priv.ctsshim
    0 : com.google.android.ext.services
    0 : com.example.android.livecubes
    0 : com.android.providers.telephony
    0 : com.google.android.onetimeinitializer
    0 : com.android.protips
    0 : com.android.externalstorage
    0 : com.android.htmlviewer
    0 : com.android.providers.downloads.ui
    0 : com.android.pacprocessor
    0 : com.android.contacts
    0 : com.android.camera2
    0 : com.android.egg
    0 : com.android.mtp
    0 : com.android.systemui.theme.dark
    0 : com.android.dreams.basic
    0 : com.android.bips
    0 : com.android.emulator.smoketests
    0 : com.google.android.apps.wallpaper.nexus
    0 : com.google.android.apps.nexuslauncher
    0 : com.android.proxyhandler
    0 : org.chromium.webview_shell
    0 : com.google.android.feedback
    0 : com.android.managedprovisioning
    0 : com.android.providers.partnerbookmarks
    0 : com.android.wallpaper.livepicker
    0 : com.google.android.sdksetup
    0 : com.google.android.backuptransport
    0 : jp.co.omronsoft.openwnn
    0 : com.android.bookmarkprovider
    0 : com.android.cts.ctsshim
    0 : com.android.wallpaperbackup
    0 : com.android.customlocale2
    0 : com.example.android.softkeyboard
    0 : com.android.development
    0 : com.android.captiveportallogin
    0 : com.android.widgetpreview
    1546496940823 : d128.work.fragmentviewpager
```

#####`adb shell bmgr backup <任意のパッケージ名>`

バックアップ待ち（Pending key/value backup）に追加することができます。
これはコードからも可能で、[BackupManager#dataChanged][3]を実行します。バックアップデータを更新したい場合などで実行します。

#####`adb shell bmgr run`

バックアップを強制的に実行します。
本来は[ドキュメント][4]にもあるように数時間おきにOSが自動的に実行するようです。
ただこのコマンドを実行してもバックアップが開始されないことがあるので、`adb shell dumpsys backup`でバックアップ状況を確認しながら、再度バックアップの実行を試みてください。

#端末のバックアップ設定

<img src="https://qiita-image-store.s3.amazonaws.com/0/149856/3e2f0db7-76d9-cf79-c582-a7039702457b.png" width=50%>

###データのバックアップ

この設定をONにすることでバックアップができるようになります。
OFFにするとバックアップしたデータが削除され、バックアップされなくなります。

###自動復元

この設定をONにすることでアプリケーションインストール時にデータが復元されます。
例えば、[SharedPreferencesBackupHelper][5]を使ってプリファレンスをバックアップしていた場合、再インストール時にプリファレンスが復元されます。

逆に自動復元をOFFにした場合は、[BackupManager#requestRestore][6]で復元要求をする必要があります。
しかし、ここで問題がたくさん発生しました。

* <font color="red">BackupManager#requestRestoreはAPI level 28からdeprecated
<br>
Android Pie以上で現在のところ復元できる見通しが立っておりません。
どなたかご助力いただけると幸いです。</font>
* 復元要求から復元完了後、アプリ再起動が必要
<br>
[SharedPreferencesBackupHelper][5]でプリファレンスをバックアップしていた場合、復元完了後すぐにプリファレンスから値を取得しても復元されていない問題が発生しました。Android Oreoでは問題ありませんでしたが、Android Nougatでこの問題がありました。
対応方法としては、[RestoreObserver#restoreFinished][7]のタイミングで[AlarmManager][8]などを使い、アプリケーションを再起動することで復元した値が取得できるようになりました。
<br>
<br>
しかし、再起動はユーザビリティを悪くしてしまうと思います。そこで、[BackupAgentHelper#onBackupとonRestore][10]をオーバーライドして自前でバックアップと復元を行うことで解決しました。

#サンプルプロジェクト

* [プリファレンスをバックアップするサンプルプロジェクト][11]
* [アプリの再起動不要の復元サンプルプロジェクト][12]


[1]:https://developer.android.com/guide/topics/data/backup
[2]:https://stackoverflow.com/questions/31436547/android-data-backup-aborted-by-rate-limiter
[3]:https://developer.android.com/reference/android/app/backup/BackupManager#dataChanged(java.lang.String)
[4]:https://developer.android.com/guide/topics/data/backup#Choosing
[5]:https://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper
[6]:https://developer.android.com/reference/android/app/backup/BackupManager#requestRestore(android.app.backup.RestoreObserver)
[7]:https://developer.android.com/reference/android/app/backup/RestoreObserver#restoreFinished(int)
[8]:https://developer.android.com/reference/android/app/AlarmManager.html
[9]:https://developer.android.com/guide/topics/data/keyvaluebackup#BackupAgent
[10]:https://developer.android.com/guide/topics/data/keyvaluebackup#BackupAgent
[11]:https://github.com/mn-D128/SharedPreferencesBackup
[12]:https://github.com/mn-D128/ExtendBackupAgentHelper