nashcft's blog

時々何か書く。

Android 13 から導入された per-app language preferences について

文脈

github.com

触ったのでついでに調べてまとめることにした。

Per-app language preferences って何

端末全体の言語設定とは別にアプリ毎の言語設定を system 側で管理してくれる機能。

developer.android.com

System settings から設定できるようにする

Android 13+ で有効化するためにやることは以下の2つ:

  • res/xml/locale_config.xml を作成する
  • AndroidManifest の application 要素に android:localeConfig 属性を追加し、作成した config xml を指定する

細かいことはドキュメントを読んでもらうとして、 locale_config.xml は以下のような内容のファイルになっている。

<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en"/>
    <locale android:name="ja"/>
    <locale android:name="zh-CN"/>
    ...
</locale-config>

ドキュメントで言及のある build.gradleresConfigs は単に per-app preferences で指定したサポートする言語とアプリに含める言語 resource を一致させるための設定なので、書かなくても有効化される。

上記の変更によって per-app language preferences が有効化されると、以下の場所からアプリの使用言語を設定できるようになる:

  • Settings > Apps > 対象のアプリ > Language
  • Settings > System > Languages & input > App Languages > 対象のアプリ

アプリ独自の言語設定機能と連携させる

以前からアプリ内の言語設定を変更する機能を独自に実装していたアプリや、言語設定のためにいちいち Settings に移動させずに自アプリ内で操作を完結させたいという要求に対して、この per-app language preferences と連携させるための API が追加されている。

追加された API は framework の LocaleManager と AndroidX AppCompat library の AppCompatDelegate#getApplicationLocales/AppCompatDelegate#setApplicationLocales で、 LocaleManager は added in API level 33 なので当然公式の推奨は AppCompat の方になる。 AppCompat library の方は 1.6.0 で追加されている (2022-09-23 時点で RC01)。

することに特に難しいことはなく、現在設定されている locale を取得する時に getApplicationLocales を呼び、 locale を変更する時は設定したい言語を LocaleList / LocaleListCompat として setApplicationLocales に渡すだけで OK。ちなみに、言語設定が system default の時は空の locale list が取得でき、 system default に設定したい時は空の locale list をセットすることで実現できる。

// アプリの言語が「日本語」に設定されているとする

// 中身は [ja]
val currentLocales = AppCompatDelegate.getApplicationLocales()

// アプリの言語設定が「English」になる
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("en"))

// アプリの言語設定が「System Default」になる
// LocaleListCompat.forLanguageTags("") も getEmptyLocaleList() と同義になる
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())

// 中身は []
val localesChangedToDefault = AppCompatDelegate.getApplicationLocales()

以下は DroidKaigi2022 のアプリで実装したもののデモ動画。アプリ内で変更した言語設定が system settings の方の言語設定にも反映されていることがわかる。

AppCompat の API を使う際の注意点

追記 (2022-09-24): Android 12 以下での挙動の記述が誤っていたので修正

あまり無いケースだと思うが AppCompatDelegate の方を使う場合の注意点として、API reference にも記述があるように、getApplicationLocales/setApplicationLocalesActivity#onCreate 以降でのみ呼ぶようにすべきというものがある。これは Android 13+ 向けの実装の都合で、内部で LocaleManager を取得するのに必要な Context が手に入るようになるのが onCreate 以降*1となり、それまでは呼んでも実際の get/set が実行されないから。 Android 12 以下の場合は get/set で受け渡しする値は AppCompatDelegate 内部のメンバとして管理されているため、 onCreate 以前でも AppCompatDelegate との値の受け渡しは正常に行うことができる。この際、実際に framework 側に設定が同期されるのは onCreate 呼び出し時である。後述するがこの挙動を利用して Android 12 以下向けに後方互換対応をする場合がある。

また、これは地味かつ当然といえば当然な話だが、 AppCompat の API を使う時はそれが呼ばれる Activity が AppCompatActivity を継承したものである必要がある*2AppCompatActivity が用いられなかった場合の挙動は大体上記と同じだが、こちらに関しては Android 12 以下でも framework に言語設定が同期されなくなるので設定の変更がアプリに反映されない。

この辺の仕組みの詳細は長くなるし横道に逸れるので書かないが気になる人は AppCompatDelegatesAppContextsActivityDelegates に入る値はどこから来るかや framework との同期はどこで行われるかを追いかけてみるとわかると思う。

Android 12 以下でも使えるようにする

Android 12 以下には per-app language preferences の機能が存在しないので、「アプリ独自の言語設定機能と連携させる」で書いたように AppCompat library を使ってアプリ内に言語設定の機能を実装する必要があるが、これだけだとアプリのプロセスが終了する度に設定した言語の情報が揮発してしまうので、アプリ内で設定情報を保存しておく必要がある。これに関しては AppCompat library を使った自動保存を opt-in するか自前の保存機構を使って起動時に連携するかという二通りの対応方法がある。

言及箇所へのリンク:

自動保存の opt-in

Manifest に以下のように AppLocalesMetadataHolderService を登録するとライブラリの方で上手くやってくれる。ただしこの設定をすると blocking な disk read/write が発生するようで、 StrictMode で検知するようにしている場合は引っかかってしまうとのこと。 DroidKaigi2022 アプリは自前の言語設定機能をまだ持っていなかったし、新規にローンチされるものでデータの引き継ぎを考慮する必要もなかったのでこちらを採用した。

<application
  ...
  <service
    android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
    android:enabled="false"
    android:exported="false">
    <meta-data
      android:name="autoStoreLocales"
      android:value="true" />
  </service>
  ...
</application>

自前の保存機構と連携

既に自前の言語設定機能を持っていたり、 blocking な disk read/write が許容できないなどの理由で AppLocalesMetadataHolderService を manifest に登録しないか、した上で autoStoreLocalesfalse した場合はこちらの選択肢となる。やることは以下の通り:

  1. Activity の Activity#onCreate が呼ばれる前*3に保存されている言語設定を読み出して AppCompatDelegate#setApplicationLocales に与える
  2. 上記の操作を Android 13+ で実行されないように制御する
  3. アプリ操作での setApplicationLocales 実行時に自前の storage にも設定を保存する

Android 13+ では同期処理を実行させないようにするのは、 per-app language preferences の方で言語設定を保存しているので初期化する必要がないのと、1. の時点で呼んでも機能しないから。

バイスの OS version 更新時の設定引き継ぎ

アプリ利用中にデバイスの OS が 12 から 13 に更新された際にそれまでアプリ内で管理していた言語設定を per-app language preferences に引き継ぐ方法について:

  • autoStoreLocales を有効にする対応をした場合は AppCompat library が自動的に同期してくれる
  • 自前の storage で管理していた場合は適当なタイミングで設定を取り出して AppCompatDelegate#setApplicationLocales に与える one-time の処理を仕込んでおく

まとめ

  • Android 13 以降でだけ動けばいいなら xml 作って manifest に設定すれば OK
  • 12 以下でも同じような体験をさせたい場合はアプリに言語設定機能を作って AppCompat library を使う、12 以下向けの設定保存をどうするか考える
    • AppCompat library に設定保存まで任せると実装や OS version up 時の引き継ぎが楽、ただし blocking disk R/W を許容する必要がある
    • 設定保存をアプリで実装して設定の取得/変更をする時だけ AppCompat の API を呼ぶ方法もある
  • Android 12 以下から 13 以降への更新時に言語設定を per-app language preferences に引き継ぐことを考慮しよう

*1:より正確には androidx.activity.ComponentActivity#onCreate 以降

*2:実際には AppCompat の API が呼ばれるまでの経路中で AppCompatActivity が起動されていれば呼び出し元が乗ってる Activity 自体は AppCompatActivity でなくてもよさそう感がある

*3:Application#onCreate とか Activity#attachBaseContext とか