rakuishi.com

Android 13(API レベル 33)にアプリを移行する

例年通り 9 月に Android メジャーバージョンが公開されると思いきや、今年は 8 月中旬に来ましたね。ということで今週はお仕事のアプリを Android 13 対応していました。

公式の実装ガイドも日本語化されており非常に理解しやすいのですが、実際に動くサンプルコードが一緒に例示されていればより良いですよね。この記事では、自分が対応した移行項目とそのサンプルコードを載せていきます。

テーマ別アプリアイコン対応

今まで Google 純正アプリのみ対応していたテーマアイコンが、3rd Party 製アプリも対応できるようになりました。ic_launcher.xml ファイルに monochrome タグを追加するだけの簡単実装です。

壁紙とスタイル → テーマアイコン(ベータ版)より確認できます。

<!-- ic_launcher.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
  <background android:drawable="@drawable/ic_background" />
  <foreground android:drawable="@drawable/ic_foreground" />
  <!-- NEW -->
  <monochrome android:drawable="@drawable/ic_foreground" />
</adaptive-icon>

アプリ別言語設定

端末の言語は英語だけれど、このアプリは日本語で使いたいというシチュエーションに対応できるようになります。res/xml/locales_config.xml ファイルに対応言語を書いて、AndroidManifest.xml にこの設定ファイルを加えるだけで使えます。

<!-- res/xml/locales_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-config>
<!-- AndroidManifest.xml -->
<application
  android:localeConfig="@xml/locales_config"
  /* 省略 */
  >

コピー確認 UI の重複を修正

Android 13 では、左下にコピー内容が表示されるようになりました。だいたいのアプリではコピー完了したことをトーストやスナックバーで知らせる実装になっていると思いますが、その左下のコピー内容と被ってしまいます。Android 13 ではトーストやスナックバーを表示しないように修正します。

fun copyText(context: Context, text: String?, @StringRes toastMessageResId: Int?) {
  val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return
  val clipData = ClipData.newPlainText("text", text)
  clipboard.setPrimaryClip(clipData)
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && toastMessageResId != null) {
    // Android 13 以降はコピー内容が OS で案内されるため、成功トーストを表示する必要はない
    Toast.makeText(context, toastMessageResId, Toast.LENGTH_LONG).show()
  }
}

広告 ID

Android 13 以降、広告 ID を使っているアプリはパーミッションの記載が必要になるそうです。また、Google Play Console のアプリ → ポリシー → アプリのコンテンツ → 広告 ID より審査が必要になります。

API レベル 33(Android 13)以降をターゲットとし、広告 ID を使用するアプリについては、そのアプリの AndroidManifest.xml で標準の権限 com.google.android.gms.permission.AD_ID を宣言する必要があります。

<!-- AndroidManifest.xml -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID">

通知権限を取得

iOS ではもともと通知権限が必要でしたが Android も倣うようになりました。Android 13 対応をしないままだと、アプリインストール後の初回起動時に通知権限ダイアログが表示されてしまいます。任意のタイミングで通知権限を取得するには、以下のようなコードを追加します。

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
// Activity.kt
private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
    /* 成功 or 失敗のケースをここに書く */
  }

fun launchNotificationPermissionIfNeeded() {
  val hasNoPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
  && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
  if (hasNoPermission) {
    notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
  }
}

メディア権限の取得方法を修正

今まで画像ファイルを取得するには READ_EXTERNAL_STORAGE 権限が必要でしたが、READ_MEDIA_IMAGES に変更になります。

ちなみに動画は READ_MEDIA_VIDEO, 音声ファイルは READ_MEDIA_AUDIO となります。大雑把な権限が Android 13 で細分化されたイメージだと思います。

<!-- AndroidManifest.xml -->
<uses-permission
  android:name="android.permission.READ_EXTERNAL_STORAGE"
  android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
// Activity.kt
private val readImagesPermission =
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.READ_MEDIA_IMAGES
  else Manifest.permission.READ_EXTERNAL_STORAGE

private val readImagesPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
  val hasPermission = ContextCompat.checkSelfPermission(this, readImagesPermission) == PackageManager.PERMISSION_GRANTED
    if (hasPermission) {
      // you can pick images
    } else {
      // has no permission
    }
  }

fun launchReadImagesPermission() {
  val hasPermission = ContextCompat.checkSelfPermission(this, readImagesPermission) == PackageManager.PERMISSION_GRANTED
  if (hasPermission) {
    // you can pick images
  } else {
    readImagesPermissionLauncher.launch(readImagesPermission)
  }
}

写真選択ピッカーを表示

写真選択ピッカーが装い新たになりました。今までの Intent を少し変えれば半モーダルで開くピッカーを表示できます。

// Activity.kt
private val imagePickLauncher: ActivityResultLauncher<Intent> =
  registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    if (it.resultCode != RESULT_OK) return@registerForActivityResult
    it.data?.data?.let { uri ->
      // do something
    }
  }

fun launchImagePick() {
  // 権限を「メディア権限の取得方法を修正」を参考に取得しておく
  val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
    // 複数画像の場合は `images/*`, 動画の場合は `video/*` を指定
    intent.type = "image/*"
    intent
  } else {
    Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
  }
  imagePickLauncher.launch(intent)
}