Android では設定画面を簡単に作れるけど、どうすればダイレクトブート対応にできるかの解説があまり見当たらなかったので、調べてみた。
背景
Android では、SharedPreferences
クラスで設定値の読み書きができるほか、PreferenceActivity
や PreferenceFragment
などのクラスで設定画面も簡単に作れる。
ところが、Android 7.0 (Nougat) 以降で、ダイレクトブート (direct boot) という機能が導入された。端末の起動直後は、端末暗号化ストレージ (device encrypted storage) のファイルにしかアクセスしかない。ユーザーが端末をアンロックすると、認証情報暗号化ストレージ (credential encrypted storage) にもアクセスできるようになる。
これで困るのが、バックグラウンドで何かしたいアプリだ。例えば AlarmManager
に登録したアラームは永続化されないため、再起動すると消えてしまう。そのため、再起動後にアラームを登録し直す必要があるんだけど、起動直後は端末暗号化ストレージにしかアクセスできない。加えて、その後もアンロックされなければ、アラームで設定した時間になっても同じかもしれない。
だからアラームの情報は端末暗号化ストレージに持ちたいのだけど、設定画面を普通に作ると認証情報暗号化ストレージに保存されてしまう。
端末暗号化ストレージに保存されるようにするには、プログラムで対応する必要があるようだ。
対応方法
ストレージ切り替えの準備
端末暗号化ストレージは Android 7.0 (Nougat) より前のバージョンでは使えない。それもサポートするためには、Android のバージョンによって保存先を切り替える必要がある。
切り替えるには、設定情報を読み書きするときに普通の Context
そのものではなく、createDeviceProtectedStorageContext
メソッドを呼んで得られたものを使う。
ただ、バージョンの判定をいちいちやるのは面倒なので、先に作っておく。
Kotlin の拡張メソッドを使うとこんな感じ。
fun Context.createPreferenceStorageContext(): Context { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { createDeviceProtectedStorageContext() } else { this } }
Java だったら static
メソッドでこんな感じだろうか。
public static createPreferenceStorageContext(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return context.createDeviceProtectedStorageContext(); } else { return context; } }
設定画面の修正
設定画面の読み書き先を端末暗号化ストレージに切り替えるには、設定画面の Fragment
で、PreferenceManager
の setStorageDeviceProtected
メソッドを呼ぶ。
設定情報へのアクセスが発生する前に呼んでおく必要があるので、onCreate
メソッドの最初が安全そうだ。
このメソッドは Android 7.0 (Nougat) 以降にしかないので、以前のバージョンもサポートする場合には if
文で囲む必要がある。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { preferenceManager.setStorageDeviceProtected() } // 以下略 }
また、設定画面の中で、他にも設定情報を見ている箇所を探して、端末暗号化ストレージを使えるように修正する必要がある。
例えば、設定値のサマリーを表示するために、設定情報を呼んでいる箇所がそうだ。
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.context)
.getString(preference.key, ""))
このうち preference.context
の部分をそのままにすると、認証情報暗号化ストレージから値を取ろうとしてしまう。そこで、先ほど作ったストレージ判定メソッドを使って、端末暗号化ストレージを使うようにする。
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager
.getDefaultSharedPreferences(preference.context.createPreferenceStorageContext())
.getString(preference.key, ""))
Java の場合は、preference.getContext()
を createPreferenceStorageContext(preference.getContext())
のように直すことになるだろう。
これで、設定画面は端末暗号化ストレージを使えるようになる。
設定を自前で読み書きするコードの修正
設定画面以外で、設定値を読み書きしている箇所を修正していく。
具体的には、SharedPreferences
を取得している箇所を探す。
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
この引数に渡している Context
を、やはり先ほどと同じように自作のメソッドで置き換えていく。
val prefs = PreferenceManager.getDefaultSharedPreferences(
context.createPreferenceStorageContext())
Java だとこんな感じ。
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( createPreferenceStorageContext(context));
ストレージの移行
認証情報暗号化ストレージに保存していたファイルを、端末暗号化ストレージに移さないといけない場合がある。
ユーザーが端末の OS をバージョンアップしたとか、以前からあるアプリが新たにダイレクトブートに対応するとか。
これは、Context
クラスの moveSharedPreferencesFrom
メソッドで行える。設定情報のファイルを、端末暗号化ストレージに移動してくれる。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val srcContext = this val destContext = context.createPreferenceStorageContext() val prefName = PreferenceManager.getDefaultSharedPreferencesName(srcContext) destContext.moveSharedPreferencesFrom(srcContext, prefName) }
参考にしたサイト
Google によるサンプルコード
ダイレクトブート対応で、設定情報の取得・参照や、端末起動の検知を行う場合のコード例。
Google の開発者向けガイド
ダイレクトブートについての説明。注意点なども載っている。ただし、途中のコード例に致命的な誤植があるので、英語版も合わせて参照したい。