Posted at

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


前述

Androidでは、再インストールや機種変でアプリケーションを入れ直したとき前回使用時の状態に復元するバックアップ機能が用意されています。

詳しくはこちらの公式ドキュメントをご覧ください。

バックアップ機能には、次の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


    Googleのクラウド上ではなく端末のローカルにバックアップされます

  • com.google.android.gms/.backup.migrate.service.D2dTransport


    不明

  • com.google.android.gms/.backup.BackupTransportService


    Googleアカウントに紐付き、クラウド上にバックアップされます


adb shell bmgr transport <任意のtransport>

バックアップ先を切り替えることができます。

adb shell bmgr list transportsで出力されたものの中から選択することができます。

adbコマンド以外からは切り替える方法がないため、開発中のテストで使用するものだと考えられます。

私が利用したケースとして、Android 5.1.1の端末においてバックアップテストを繰り返していると、復元をしても最新のバックアップデータで復元されない問題が発生しました。Logcatを確認していると、rate limiterというログが表示されバックアップ制限に引っかかっていることがわかりました。その際にバックアップ先をLocalTransportに変更してテストを行いました。そのときに参考になったstack overflowがこちら

$ 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


    バックアップされているアプリケーションが表示されます

  • Pending key/value backup: XXX


    バックアップ待ちのアプリケーションが表示されます
    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を実行します。バックアップデータを更新したい場合などで実行します。


adb shell bmgr run

バックアップを強制的に実行します。

本来はドキュメントにもあるように数時間おきにOSが自動的に実行するようです。

ただこのコマンドを実行してもバックアップが開始されないことがあるので、adb shell dumpsys backupでバックアップ状況を確認しながら、再度バックアップの実行を試みてください。


端末のバックアップ設定


データのバックアップ

この設定をONにすることでバックアップができるようになります。

OFFにするとバックアップしたデータが削除され、バックアップされなくなります。


自動復元

この設定をONにすることでアプリケーションインストール時にデータが復元されます。

例えば、SharedPreferencesBackupHelperを使ってプリファレンスをバックアップしていた場合、再インストール時にプリファレンスが復元されます。

逆に自動復元をOFFにした場合は、BackupManager#requestRestoreで復元要求をする必要があります。

しかし、ここで問題がたくさん発生しました。


  • BackupManager#requestRestoreはAPI level 28からdeprecated


    Android Pie以上で現在のところ復元できる見通しが立っておりません。
    どなたかご助力いただけると幸いです。

  • 復元要求から復元完了後、アプリ再起動が必要


    SharedPreferencesBackupHelperでプリファレンスをバックアップしていた場合、復元完了後すぐにプリファレンスから値を取得しても復元されていない問題が発生しました。Android Oreoでは問題ありませんでしたが、Android Nougatでこの問題がありました。
    対応方法としては、RestoreObserver#restoreFinishedのタイミングでAlarmManagerなどを使い、アプリケーションを再起動することで復元した値が取得できるようになりました。




    しかし、再起動はユーザビリティを悪くしてしまうと思います。そこで、BackupAgentHelper#onBackupとonRestoreをオーバーライドして自前でバックアップと復元を行うことで解決しました。


サンプルプロジェクト