20210728のAndroidに関する記事は9件です。

Android: Android 10 からの対象範囲別ストレージ (Scoped Storage) とメディアファイルアクセスのまとめ

Android 10 から、対象範囲別ストレージ (Scoped Storage) が導入され、アプリ固有のストレージと、アプリ外部のストレージが厳密に区別され扱われるようになりました。 アプリ外部の画像を選択したり、外部のアプリと共有可能なメディア領域へ画像を保存したりする場合には対象範囲別ストレージを正しく扱う必要があります。 Andorid 11 以上の環境では、Android 11 対応 (targetSdkVersion = 30) でアプリをビルドすると対象範囲別ストレージが強制的に有効となります。requestLegacyExternalStorage 設定も無視されるようになります。対象範囲別ストレージに正しく対応しましょう。 この記事では Android 10 以上の対象範囲別ストレージ環境を前提としています。Android 9 以下の違いを考慮する必要がある箇所では都度補足しています。 対象範囲別ストレージのまとめ 対象範囲別ストレージは 3 種類のストレージがあります。 アプリ固有ストレージ メディア領域 その他のストレージ アプリ固有ストレージ アプリ固有のストレージへは特別な権限なくアクセスが可能です。アプリ固有のストレージはアプリアンインストール時にデータ削除されます。 アプリ固有のストレージ一覧 API 領域 Context.filesDir アプリ固有内部ストレージ File ディレクトリ Context.cacheDir アプリ固有内部ストレージ Cache ディレクトリ Context.getExternalFilesDir() アプリ固有外部ストレージ File ディレクトリ Context.externalCacheDir アプリ固有外部ストレージ Cache ディレクトリ メディア領域 外部のアプリから共有可能なメディア領域は MediaStoreAPI 経由でアクセスします。MediaStoreAPI では、OS によってファイルがデータベース管理されており、Content Provider 経由でデータベースやファイルへアクセスします。以下の種類のファイルとメタデータを扱えます。 画像 (MediaStore.Imagesテーブル) 動画 (MediaStore.Videoテーブル) 音声 (MediaStore.Audioテーブル) ダウンロードファイル (MediaStore.Downloadsテーブル) メディア領域へ保存したファイルはアプリをアンインストールしても削除されません。 メディア領域へのファイルの 書き込みに権限は不要 です。MediaStoreAPI 経由でファイルを書き込めば、そのファイルは他のアプリから見えるようになります。 メディア領域からファイルを読み込む場合は、権限によって以下の動作となります。 権限の状態 MediaStore の読み取り READ_EXTERNAL_STORAGE 権限なし 他のアプリが保存したメディアファイルは見えず、自アプリが保存したメディアファイルだけを取得できる。アプリを再インストールすると、アンインストール前に自アプリが保存したメディアファイルも取得できなくなる。 READ_EXTERNAL_STORAGE 権限あり 他のアプリが保存したメディアファイルも含めて、すべてのメディアファイルが取得できる。 その他のストレージ ドキュメントなど、ファイルやディレクトリを直接扱いたい場合は ストレージアクセスフレームワーク を利用できます。 この記事ではストレージアクセスフレームワークについては説明しません。 要件ごとの解説 対象範囲別ストレージ環境で、実現したい要件ごとに解説します。 公式ドキュメントにも、ユースケースと Android バージョンを考慮した実装の使い分けがまとまっています。こちらも参照してみてください。 外部のアプリからアクセスできるように画像を保存したい Android 10 以上でも、Android 9 以下でも共通の実装となりますが、Android 9 以下では権限が必要になります。 Android 10 以上では、MediaStoreAPI 経由で画像ファイルを保存します。WRITE_EXTERNAL_STORAGE 権限は不要です。 Android 9 以下では、WRITE_EXTERNAL_STORAGE 権限を得てから MediaStoreAPI 経由で画像ファイルを保存します。 詳細は以下の記事を参照してください。 アプリ外の画像を選択させて、アプリから読み取りたい Android 10 以上でも、Android 9 以下でも共通の実装となります。 アプリ外の画像選択は外部の画像選択アプリを Intent で呼び出します。 ユーザーが外部の画像選択アプリ経由で画像を選択するため、 READ_EXTERNAL_STORAGE 権限は不要です。 アプリ外の画像一覧を取得して、アプリ内で画像を表示したい Android 10 以上でも、Android 9 以下でも共通の実装になります。(Android 9 以下では MediaStoreAPI の扱いが少しだけ異なるところはあります) 画像選択アプリを経由せずに、直接アプリ内で画像を表示したい場合は MediaStoreAPI 経由でファイルを取得します。アプリ外の画像一覧を取得するためには READ_EXTERNAL_STORAGE 権限が必要です。 詳細は以下の記事を参照してください。 カメラアプリで写真を撮影し、その画像を取得したい Android 10 以上でも、Android 9 以下でも共通の実装になります。 外部のカメラアプリを Intent で呼び出し、自アプリの アプリ固有ストレージ に画像ファイルを保存させます。 カメラ機能に直接アクセスするわけではないため CAMERA 権限は不要です。アプリ固有ストレージへファイルを書き込むため、WRITE_EXTERNAL_STORAGE 権限は不要です。 ただし、カメラアプリからアプリ固有ストレージへ画像ファイルを書き込ませるために FileProvider の設定が必要になります。 このとき、撮影された画像ファイルはアプリ固有ストレージに存在するため、メディア領域へは記録されず、外部のアプリからはアクセスできません。また、アプリをアンインストールすると画像は削除されます。OCR 画像認識や身分証写真撮影など、そのアプリ内でしか使わず、画像が消えてもユーザーが困ることはないと断言できる用途であればこの対応でも構いません。 詳細は以下の記事を参照してください。 カメラアプリで写真を撮影し、その写真を外部のアプリからもアクセスできるようにしたい Android 10 以上でも、Android 9 以下でも共通の実装となりますが、Android 9 以下では権限が必要になります。 Android 10 以上では、外部のカメラアプリを Intent で呼び出し、自アプリの アプリ固有ストレージ に画像ファイルを保存させ、その後 MediaStoreAPI 経由で メディア領域 へファイルを書き込みます。CAMERA 権限は不要。WRITE_EXTERNAL_STORAGE 権限も不要。FileProvider の設定だけが必要です。 Android 9 以下でも、上記手順通りですが、MediaStoreAPI でメディア領域へファイルを書き込むために WRITE_EXTERNAL_STORAGE 権限が必要になります。 アプリ内ストレージに保存してしまうとアプリアンインストールで写真も消えてしまいます。ライフログやブログ記事へ添付する写真など、写真そのものが重要なデータである場合は、メディア領域へ保存することでアプリをアンインストールしても写真が消えないようにしてあげると親切です。 詳細は以下の記事を参照してください。 カメラアプリで撮影するか、画像選択アプリから画像を選択するかをユーザーに選ばせたい カメラアプリ連携 Intent と画像選択 Intent を組み合わせて、Intent Chooser を表示します。 必要な権限や実装の詳細は組み合わせる Intent によります。 詳細は以下の記事を参照してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Could not find tools.jar. Please check that /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home contains a valid JDK installation.

しばらく開発していなかったAndroidアプリが以下のエラーでビルドできなくなってしまいました。 Could not find tools.jar. Please check that /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home contains a valid JDK installation. Stackoverflow によると Big sur に更新後に発生する様子。 ちなみに私の環境は2番目の回答のような2つのJava環境がある(?はっきりわかってない)状態でした。 $ /usr/libexec/java_home -V | grep jdk Matching Java Virtual Machines (2): 1.8.181.13 (x86_64) "Oracle Corporation" - "Java" /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home 1.8.0_181 (x86_64) "Oracle Corporation" - "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home 対処方法は下の方のパスをJAVA_HOMEとして環境変数に設定する方法が有効でした。 ~/.bash_profile # 以下追記 export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android: ActivityResultContract を使って画像選択アプリで画像を選択する

ActivityResultContract を使って、外部の画像選択アプリを起動し、ユーザーに画像を選択させます。 Android 標準の画像選択アプリではなく Google Photos アプリを起動して画像選択させたい場合は以下の記事を参照してください。 必要な権限 外部の画像選択アプリによって画像を選択し、その画像を読み取るためには READ_EXTERNAL_STORAGE 権限は不要 です。 画像選択アプリが端末に保存されている画像を読み取る権限を取得しており、ユーザーが選択した画像は Intent 発行元のアプリが読み取れるパスとして返却されるためです。 前提 AndroidX や AndroidX Fragment が導入されている前提です。 実装 class MyFragment: Fragment() { private val getContentLauncher = registerForActivityResult( ActivityResultContracts.GetContent() ) { uri -> if (uri != null) { // 画像が選択された // content://com.android.providers.media.documents/... // のような Content Provider 形式の URI を受け取ります } else { // 画像選択がキャンセルされた } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { getContentLauncher.launch("image/*") } } // ... 解説 Fragment.registerForActivityResult() を実行しておくことで、Fragment が Activity に Attach されたときに、Activity の ActivityResultRegistry に GetContent Contract が登録されます。ActivityResultRegistry に登録されると、ActivityContract が onActivityResult() を受け取って処理できるようになります。 Contract を登録すると、任意のタイミングで getContentLauncher.launch() を実行することで、コンテンツ選択の Intent が発行されます。 GetContent Contract は以下の Intent を発行しています。 ActivityResultContracts.java Intent(Intent.ACTION_GET_CONTENT) .addCategory(Intent.CATEGORY_OPENABLE) .setType(input) // ここでは input = "image/*" registerForActivityResult() は内部で onActivityResult の RequestCode を連番として生成しています。 registerForActivityResult() を実行する順番は常に同じである必要があります。条件分岐などを使わないように気をつけながら Activity や Fragment のフィールドとして初期化してしまうのが安全です。 実行結果 以下のように画像選択アプリが起動します。Android 標準の画像選択アプリであれば、Google Drive や Google Photos からの選択もできるようになっています。インストールされていれば Dropbox なども表示されます。端末にインストールされている多数のストレージアプリと連携できるため、基本的には GetContent Contract で Android 標準の画像選択アプリを呼び出すことをお勧めします。 Intent Chooser で画像選択とカメラアプリでの写真撮影を選べるようにする ActivityResultContract で Intent Chooser に画像選択とカメラアプリ起動での写真撮影の両方を表示する方法は以下の記事を参照してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android: ActivityResultContract を使って Google Photos アプリで画像を選択する

ActivityResultContract を使って、Google Photos アプリを起動し、ユーザーに画像を選択させます。 ActivityResultContracts.GetContent を使って、Android 標準の画像選択アプリから画像選択をする実装は以下の記事を参照してください。Android 標準の画像選択アプリであれば Google Drive や Google Photos や Dropbox など多くのストレージサービスとの連携が可能であるため、Google Photos アプリ起動 Intent よりも Android 標準の画像選択を利用することをお勧めします。 必要な権限 外部の画像選択アプリによって画像を選択し、その画像を読み取るためには READ_EXTERNAL_STORAGE 権限は不要 です。 画像選択アプリが端末に保存されている画像を読み取る権限を取得しており、ユーザーが選択した画像は Intent 発行元のアプリが読み取れるパスとして返却されるためです。 前提 AndroidX や AndroidX Fragment が導入されている前提です。 実装 Google Photos アプリが端末にインストールされているか調べるために、resolveActivity() を使用しています。Android 11 以上の Package Visibility に対応するため、AndroidManifest.xml に <queries> タグを追加します。 Android 11 で試したところ Google Photos アプリの検出には <data> タグの指定は必須です。 <data> タグの指定がない場合は resolveActivity() が null となってしまうようです。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <queries> <!-- Intent Action ではなく package で指定することもできる <package android:name="com.google.android.apps.photos" /> --> <intent> <action android:name="android.intent.action.PICK" /> <data android:mimeType="image/*" /> </intent> </queries> <application android:theme="@stylve/AppTheme"> <!-- ... --> </application> </manifest> Google Photos アプリの起動 Intent に対応した PickImageContract を実装します。 class PickImageContract : ActivityResultContract<Unit, String?>() { companion object { fun canPickImage(context: Context): Boolean { return (createIntentInternal().resolveActivity(context.packageManager) != null) } private fun createIntentInternal(): Intent { return Intent(Intent.ACTION_PICK).setType("image/*") // 厳密に Google Photos アプリだけに絞るなら Package 指定を追加しても良い // return Intent(Intent.ACTION_PICK).setType("image/*") // .setPackage("com.google.android.apps.photos") } } override fun createIntent(context: Context, input: Unit): Intent { return createIntentInternal() } override fun parseResult(resultCode: Int, intent: Intent?): String? { return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null } } PickImageContract を使用して画像選択を実行します。 class MyFragment: Fragment() { private val pickImageLauncher = registerForActivityResult(PickImageContract()) { uri -> if (uri != null) { // 画像が選択された // content://com.google.android.apps.photos.contentprovider/... // のような Content Provider 形式の URI を受け取ります } else { // 画像選択がキャンセルされた } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { if (PickImageContract.canPickImage(this)) { launcher.launch(Unit) } } } // ... 解説 ACTION_PICK Intent と mimeType "image/*" を組み合わせることで Google Photos アプリを画像選択アプリとして起動することができます。 端末に Google Photos アプリがインストールされていなければ ActivityNotFoundException が発生するため、PickImageContract の実行前に canPickImage() で Google Photos アプリのインストール確認をしています。 Intent Chooser で Android 標準画像選択アプリと Google Photos アプリをユーザーが選択できるようにする ActivityResultContract で Intent Chooser に Android 標準画像選択アプリと Google Photos アプリの両方を表示する方法は以下の記事を参照してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】URLからファイルをダウンロードして表示

はじめに URLからpdfなどのファイルをダウンロードして、ダウンロード完了後に表示する方法について記載。 以下の2つの方法を紹介。 ・コルーチンを使用した方法 ・DownloadManagerを使用した方法 コルーチンを使用した方法 事前にFileProviderを使うように設定する必要あり。 まず、コルーチンを使用するための設定。 build.gradle dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1' } そして、ダウンロード用のメソッドを作成。 fun download() { try { // linkにファイルが置かれたURLを指定 val link = "https://example.com/example.pdf" //getExternalStoragePublicDirectoryはAndroid Qで非推奨になっているため、適宜変更。 val dir = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS).absolutePath val path = dir + link.substring(link.lastIndexOf("/")) //ここでファイルをサーバからダウンロード URL(link).openStream().copyTo(FileOutputStream(File(path))) //intentによりファイルを開く val intent = Intent(Intent.ACTION_VIEW) val uri = FileProvider.getUriForFile(this@MainActivity, BuildConfig.APPLICATION_ID + ".provider", File(path)) intent.setDataAndType(uri, contentResolver.getType(uri)) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) startActivity(intent) } catch (e: Exception) { Log.e(localClassName, "Cancel", e) } } 次に、ダウンロードしたいタイミングで以下を記述。 CoroutineScope(Dispatchers.Default).launch { download() } DownloadManagerを使用した方法 先ほどと同様、まずはダウンロード用のメソッドを作成。 fun download() { try { // linkにファイルが置かれたURLを指定 val link = "https://example.com/example.pdf" val fileName = link.substring(link.lastIndexOf("/") + 1) //DownloadManagerを使用してファイルをダウンロード val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val request = DownloadManager.Request(Uri.parse(link)) request.setTitle(fileName) request.setDescription("Downloading") request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) request.setDestinationInExternalPublicDir(DIRECTORY_DOWNLOADS, fileName) val downloadId = manager.enqueue(request) val receiver = object : BroadcastReceiver() { //ダウンロード完了後の処理 override fun onReceive(context: Context, intent: Intent) { val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) if (id == downloadId) { //intentによりファイルを開く val openFileIntent = Intent(Intent.ACTION_VIEW) val uri = manager.getUriForDownloadedFile(id) openFileIntent.setDataAndType(uri, contentResolver.getType(uri)) openFileIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) startActivity(openFileIntent) } } } registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) } catch (e: Exception) { Log.e(localClassName, "Cancel", e) } } 次に、ダウンロードしたいタイミングで以下を記述。こちらはコルーチンで囲わない。 download()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数の ActivityResultContract を IntentChooser でまとめて、カメラアプリでの撮影かメディア選択のどちらにも対応する

ActivityResultContract を使って、画像選択またはカメラアプリ起動の IntentChooser を表示し、ユーザーがどちらも選べるようにします。カメラアプリが存在しない端末であれば、画像選択のみを表示します。 ActivityResultContract の仕組みは便利ですが、複数の ActivityResultContract をまとめる仕組みはありません。複数の ActivityResultContract をうまくマージするような ImageChooserContract を実装して対応します。 ActivityResultContracts.GetContent と以下の記事の TakePictureContract を組み合わせて実装します。本記事では TakePictureContract の実装について詳細な解説は省いています。詳細な解説は以下の記事を参照してください。 前提 AndroidX を導入していること。 権限について GetContent による画像選択に 必要な権限はありません。カメラアプリ起動による写真撮影も 必要な権限はありません。 カメラアプリを起動するため、CAMERA 権限も不要ですが、AndroidManifest.xml に CAMERA 権限を定義している場合のみ、CAMERA 権限のランタイムパーミッション取得が必要です。 実装 カメラアプリがアプリ固有ストレージへ書き込めるように、FileProvider の設定を追加します。 src/main/res/xml/provider_path.xml <paths> <!-- file-path: getFilesDir() 用の宣言 --> <file-path name="files" path="." /> </paths> AndroidManifest.xml に FileProvider の設定を追加します。 カメラアプリの存在確認では resolveActivity() を使用します。Android 11 以上の Package Visibility に対応するため、<queries> タグも追加します。 AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <queries> <intent> <action android:name="android.media.action.IMAGE_CAPTURE" /> </intent> </queries> <application android:theme="@stylve/AppTheme"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_path" /> </provider> <!-- ... --> </application> </manifest> TakePictureContract を実装します。以下の記事のものとほぼ同じものですが、カメラアプリが選択されなかった場合に空のファイルを削除する cleanOutputFile() を追加しています。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String ) : ActivityResultContract<String, String?>() { private var cameraOutputFile: File? = null init { owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = owner.savedStateRegistry registry.registerSavedStateProvider(savedStateKey) { bundleOf("output" to cameraOutputFile?.toString()) } registry.consumeRestoredStateForKey(savedStateKey)?.getString("output")?.let { output -> cameraOutputFile = File(output) } } }) } override fun createIntent(context: Context, input: String): Intent { val file = File( // /data/user/0/.../files のようなアプリ固有の内部ストレージ context.filesDir, input ).also { cameraOutputFile = it } file.createNewFile() // uri: content://{packageName}.fileprovider/files/Pictures/{input} のような Content URI val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } fun cleanOutputFile() { cameraOutputFile?.let { file -> if (file.length() == 0L) { file.delete() } } cameraOutputFile = null } } ImageChooserContract を実装します。GetContent と TakePictureContract を用いて、どちらも動作するように実装しています。 class ImageChooserContract( owner: SavedStateRegistryOwner, savedStateKey: String, contextProvider: () -> Context ) : ActivityResultContract<Unit, String?>() { private val getContentContract = ActivityResultContracts.GetContent() private val takePictureContract = TakePictureContract( owner, savedStateKey, contextProvider ) override fun createIntent(context: Context, input: Unit): Intent { val hasCameraApp = (Intent(MediaStore.ACTION_IMAGE_CAPTURE) .resolveActivity(context.packageManager) != null) val cameraIntent = if (hasCameraApp) { takePictureContract.createIntent(context, "image.jpeg") } else null val getImageIntent = getContentContract.createIntent(context, "image/*") return Intent.createChooser(getImageIntent, "画像を選択").apply { if (cameraIntent != null) { putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(cameraIntent)) } } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { // result = content://com.android.providers.media.documents/document/image... // result = content://media/external_primary/images/media/... // のような画像 Content URI result = getContentContract.parseResult(resultCode, intent)?.toString() ?: takePictureContract.parseResult(resultCode, intent) } takePictureContract.cleanOutputFile() return result } } ImageChooserContract を使用して画像選択またはカメラアプリでの写真撮影を実行します。 class MyFragment: Fragment() { private val imageChooserLauncher = registerForActivityResult( // savedStateKey: 他の savedStateKey と被らなければ OK ImageChooserContract(this, "imageChooser") ) { file -> if (file != null) { // file = content://com.android.providers.media.documents/document/image... // file = content://media/external_primary/images/media/... // のような選択した画像または撮影した画像 } else { // 画像選択がキャンセルされた } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { imageChooserLauncher.launch(Unit) } } // ... 解説 ImageChooserContract では GetContent() と TakePictureContract() を初期化して保持しておきます。 ACTION_IMAGE_CAPTURE Intent の resolveActivity() により、カメラアプリが端末にインストールされているかを確認しています。Android 11 以上では Package Visibility に従う必要があるため、AndroidManifest.xml へ <queries> を宣言しています。 createIntent() で、takePictureContract.createIntent() と getContentContract.createIntent() により、二つの Intent を生成します。 二つの Intent を createChooser() でまとめます。このとき、どちらかの Intent をメインの Intent とする必要がありますが cameraIntent はカメラアプリが存在しなければスキップするため、今回は getImageIntent をメインの Intent に設定しています。cameraIntent は EXTRA_INITIAL_INTENTS として設定することで、追加の選択肢に設定します。 parseResult() では二つの Intent の結果を処理します。ユーザーが画像を選択したのか、カメラアプリで写真を撮影したのかが不明であるため、どちらも処理する必要があります。今回は先に GetContent.parseResult() で処理し、GetContent.parseResult() が画像パスを返さなければ TakePictureContract.parseResult() を呼び出すようにしています。RESULT_OK であることはたしかであるため、この二つの内どちらかの parseResult() は画像パスを返してくれるはずです。 parseResult() を呼び出す順番は、それぞれの Intent の結果の扱いを考慮して決めます。 GetContent Contract は ACTION_GET_CONTENT Intent を発行します。 GetContent.parseResult() の内部実装は以下のとおりとなっています。 public final Uri parseResult(int resultCode, @Nullable Intent intent) { if (intent == null || resultCode != Activity.RESULT_OK) return null; return intent.getData(); } TakePictureContract は ACTION_IMAGE_CAPTURE Intent を発行し、parseResult() は以下のとおりとなっています。 override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } GetContent Contract は resultCode == RESULT_OK かつ、Intent.data != null であれば成功となり、選択した画像のパスが手に入ります。TakePictureContract は resultCode == RESULT_OK であれば成功とみなして、あらかじめ記憶していた cameraOutputFile が撮影した写真の画像ファイルであるとみなしています。 TakePictureContract の条件が緩いため、TakePictureContract.parseResult() を先に処理してしまうと常にカメラアプリで写真撮影をしたと判定されてしまいます。 複数の ActivityResultContract を組み合わせるときは parseResult() の順番に気をつけてください。 parseResult() で結果を取得したあとに takePictureContract.cleanOutputFile() でカメラアプリ向け一時ファイルの削除をしています。cleanOutputFile() ではカメラアプリ向けに作成したファイルのファイルサイズを確認し、0 bytes であればカメラアプリは選択されなかったと判断してファイルを削除しています。カメラアプリで写真を撮影していた場合はファイルサイズが 0 bytes ではないため、ファイルを削除しません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

複数の ActivityResultContract を IntentChooser でまとめて、カメラアプリでの撮影かメディア選択のどちらにも対応する (Google Photos アプリ選択にも対応)

ActivityResultContract を使って、Android 標準の画像選択アプリと Google Photos アプリ画像選択とカメラアプリ起動の IntentChooser を表示し、ユーザーがいずれかを選べるようにします。Google Photos アプリやカメラアプリが存在しない端末であれば、それぞれの選択肢は非表示となります。Google Photos アプリもカメラアプリも存在しない場合は、Intent Chooser は表示されず、Android 標準の画像選択アプリが起動します。 ActivityResultContract の仕組みは便利ですが、複数の ActivityResultContract をまとめる仕組みはありません。複数の ActivityResultContract をうまくマージするような ImageChooserContract を実装して対応します。 ActivityResultContracts.GetContent と以下の記事の PickImageContract と TakePictureContract を組み合わせて実装します。本記事では PickImageContract と TakePictureContract の実装について詳細な解説は省いています。詳細な解説は以下の記事を参照してください。 (ここにリンク追加) 前提 AndroidX を導入していること。 権限について GetContent や Google Photos アプリによる画像選択に 必要な権限はありません。カメラアプリ起動による写真撮影も 必要な権限はありません。 カメラアプリを起動するため、CAMERA 権限も不要ですが、AndroidManifest.xml に CAMERA 権限を定義している場合のみ、CAMERA 権限のランタイムパーミッション取得が必要です。 実装 カメラアプリがアプリ固有ストレージへ書き込めるように、FileProvider の設定を追加します。 src/main/res/xml/provider_path.xml <paths> <!-- files-path: getFilesDir() 用の宣言 --> <files-path name="files" path="." /> </paths> AndroidManifest.xml に FileProvider の設定を追加します。 Google Photos アプリの存在確認とカメラアプリの存在確認では resolveActivity() を使用します。Android 11 以上の Package Visibility に対応するため、<queries> タグも追加します。 AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <queries> <intent> <action android:name="android.intent.action.PICK" /> <data android:mimeType="image/*" /> </intent> <intent> <action android:name="android.media.action.IMAGE_CAPTURE" /> </intent> </queries> <application android:theme="@stylve/AppTheme"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_path" /> </provider> <!-- ... --> </application> </manifest> PickImageContract を実装します。以下の記事のものとまったく同じものです。 (リンク追加) class PickImageContract : ActivityResultContract<Unit, String?>() { companion object { fun canPickImage(context: Context): Boolean { return (createIntentInternal().resolveActivity(context.packageManager) != null) } private fun createIntentInternal(): Intent { return Intent(Intent.ACTION_PICK).setType("image/*") } } override fun createIntent(context: Context, input: Unit): Intent { return createIntentInternal() } override fun parseResult(resultCode: Int, intent: Intent?): String? { return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null } } TakePictureContract を実装します。以下の記事のものとほぼ同じものですが、カメラアプリが選択されなかった場合に空のファイルを削除する cleanOutputFile() を追加しています。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String ) : ActivityResultContract<String, String?>() { private var cameraOutputFile: File? = null init { owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = owner.savedStateRegistry registry.registerSavedStateProvider(savedStateKey) { bundleOf("output" to cameraOutputFile?.toString()) } registry.consumeRestoredStateForKey(savedStateKey)?.getString("output")?.let { output -> cameraOutputFile = File(output) } } }) } override fun createIntent(context: Context, input: String): Intent { val file = File( // /data/user/0/.../files のようなアプリ固有の内部ストレージ context.filesDir, input ).also { cameraOutputFile = it } file.createNewFile() // uri: content://{packageName}.fileprovider/files/Pictures/{input} のような Content URI val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } fun cleanOutputFile() { cameraOutputFile?.let { file -> if (file.length() == 0L) { file.delete() } } cameraOutputFile = null } } ImageChooserContract を実装します。GetContent と TakePictureContract を用いて、どちらも動作するように実装しています。 class ImageChooserContract( owner: SavedStateRegistryOwner, savedStateKey: String ) : ActivityResultContract<Unit, String?>() { private val getContentContract = ActivityResultContracts.GetContent() private val pickImageContract = PickImageContract() private val takePictureContract = TakePictureContract(owner, savedStateKey) override fun createIntent(context: Context, input: Unit): Intent { val canPickImage = PickImageContract.canPickImage(context) val hasCameraApp = (Intent(MediaStore.ACTION_IMAGE_CAPTURE) .resolveActivity(context.packageManager) != null) val getContentIntent = getContentContract.createIntent(context, "image/*") val pickImageIntent = if (canPickImage) { pickImageContract.createIntent(context, Unit) } else null val cameraIntent = if (hasCameraApp) { takePictureContract.createIntent(context, "image.jpeg") } else null return Intent.createChooser(getContentIntent, "画像を選択").apply { val extra = listOfNotNull(pickImageIntent, cameraIntent) if (extra.isNotEmpty()) { putExtra(Intent.EXTRA_INITIAL_INTENTS, extra.toTypedArray()) } } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { // result = content://com.android.providers.media.documents/document/image... // result = content://com.google.android.apps.photos.contentprovider/... // result = content://media/external_primary/images/media/... // のような画像 Content URI result = getContentContract.parseResult(resultCode, intent)?.toString() ?: pickImageContract.parseResult(resultCode, intent) ?: takePictureContract.parseResult(resultCode, intent) } takePictureContract.cleanOutputFile() return result } } ImageChooserContract を使用して画像選択か Google Photos アプリかカメラアプリでの写真撮影を実行します。 class MyFragment: Fragment() { private val imageChooserLauncher = registerForActivityResult( // savedStateKey: 他の savedStateKey と被らなければ OK ImageChooserContract(this, "imageChooser") ) { file -> if (file != null) { // file = content://com.android.providers.media.documents/document/image... // file = content://com.google.android.apps.photos.contentprovider/... // file = content://media/external_primary/images/media/... // のような選択した画像または撮影した画像 } else { // 画像選択がキャンセルされた } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { imageChooserLauncher.launch(Unit) } } // ... 解説 ImageChooserContract では GetContent() と PickImageContract() と TakePictureContract() を初期化して保持しておきます。 PickImageContract.canPickImage() では、ACTION_PICK と mimeType = "image/*" 指定の Intent を resolveActivity() することで Google Photos アプリが端末にインストールされているかを確認しています。 ACTION_IMAGE_CAPTURE Intent の resolveActivity() により、カメラアプリが端末にインストールされているかを確認しています。 Android 11 以上では resolveActivity() の使用にあたり Package Visibility に従う必要があるため、AndroidManifest.xml へ <queries> を宣言しています。 createIntent() で、getContentContract.createIntent() と pickImageContract.createIntent() takePictureContract.createIntent() によりそれぞれの Intent を生成します。 それぞれの Intent を createChooser() でまとめます。このとき、いずれかの Intent をメインの Intent とする必要があります。 pickImageIntent と cameraIntent は対象のアプリが存在しなければスキップするため、今回は getContentIntent をメインの Intent に設定しています。それ以外の Intent は EXTRA_INITIAL_INTENTS として設定することで、追加の選択肢に設定します。 parseResult() ではそれぞれの Intent の結果を処理します。ユーザーが画像を選択したのか、カメラアプリで写真を撮影したのかが不明であるため、それぞれ処理する必要があります。今回は GetContent.parseResult()、ickImageContract.parseResult()、TakePictureContract.parseResult() の順で処理しています。 parseResult() を呼び出す順番は、それぞれの Intent の結果の扱いを考慮して決めます。 GetContent Contract は ACTION_GET_CONTENT Intent を発行します。 GetContent.parseResult() の内部実装は以下のとおりとなっています。 public final Uri parseResult(int resultCode, @Nullable Intent intent) { if (intent == null || resultCode != Activity.RESULT_OK) return null; return intent.getData(); } PickImageContract は ACTION_PICK Intent を発行し、parseResult() は以下のとおりとなっています。 override fun parseResult(resultCode: Int, intent: Intent?): String? { return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null } TakePictureContract は ACTION_IMAGE_CAPTURE Intent を発行し、parseResult() は以下のとおりとなっています。 override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } GetContent Contract と PickImageContract は resultCode == RESULT_OK かつ、Intent.data != null であれば成功となり、選択した画像のパスが手に入ります。TakePictureContract は resultCode == RESULT_OK であれば成功とみなして、あらかじめ記憶していた cameraOutputFile が撮影した写真の画像ファイルであるとみなしています。 TakePictureContract の条件が緩いため、TakePictureContract.parseResult() を先に処理してしまうと常にカメラアプリで写真撮影をしたと判定されてしまいます。 複数の ActivityResultContract を組み合わせるときは parseResult() の順番に気をつけてください。 PickImageContract.parseResult() は GetContentContract.parseResult() と同じ処理であるため、どちらかの parseResult() は省略しても構いません。Android 標準の画像選択アプリが使われても、Google Photos アプリが使われても、選択された画像のパスは Intent.data に保持されていることに変わりはありません。 parseResult() で結果を取得したあとに takePictureContract.cleanOutputFile() でカメラアプリ向け一時ファイルの削除をしています。cleanOutputFile() ではカメラアプリ向けに作成したファイルのファイルサイズを確認し、0 bytes であればカメラアプリは選択されなかったと判断してファイルを削除しています。カメラアプリで写真を撮影していた場合はファイルサイズが 0 bytes ではないため、ファイルを削除しません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android: 複数の ActivityResultContract を IntentChooser でまとめて、カメラアプリでの撮影かメディア選択のどちらにも対応する (Google Photos アプリ選択にも対応)

ActivityResultContract を使って、Android 標準の画像選択アプリと Google Photos アプリ画像選択とカメラアプリ起動の IntentChooser を表示し、ユーザーがいずれかを選べるようにします。Google Photos アプリやカメラアプリが存在しない端末であれば、それぞれの選択肢は非表示となります。Google Photos アプリもカメラアプリも存在しない場合は、Intent Chooser は表示されず、Android 標準の画像選択アプリが起動します。 ActivityResultContract の仕組みは便利ですが、複数の ActivityResultContract をまとめる仕組みはありません。複数の ActivityResultContract をうまくマージするような ImageChooserContract を実装して対応します。 ActivityResultContracts.GetContent と以下の記事の PickImageContract と TakePictureContract を組み合わせて実装します。本記事では PickImageContract と TakePictureContract の実装について詳細な解説は省いています。それぞれの解説は以下の記事を参照してください。 前提 AndroidX を導入していること。 権限について GetContent や Google Photos アプリによる画像選択に 必要な権限はありません。カメラアプリ起動による写真撮影も 必要な権限はありません。 カメラアプリを起動するため、CAMERA 権限も不要ですが、AndroidManifest.xml に CAMERA 権限を定義している場合のみ、CAMERA 権限のランタイムパーミッション取得が必要です。 実装 カメラアプリがアプリ固有ストレージへ書き込めるように、FileProvider の設定を追加します。 src/main/res/xml/provider_path.xml <paths> <!-- files-path: filesDir 用の宣言 --> <files-path name="files" path="." /> </paths> AndroidManifest.xml に FileProvider の設定を追加します。 Google Photos アプリの存在確認とカメラアプリの存在確認では resolveActivity() を使用します。Android 11 以上の Package Visibility に対応するため、<queries> タグも追加します。 AndroidManifest.xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="..."> <queries> <intent> <action android:name="android.intent.action.PICK" /> <data android:mimeType="image/*" /> </intent> <intent> <action android:name="android.media.action.IMAGE_CAPTURE" /> </intent> </queries> <application android:theme="@stylve/AppTheme"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_path" /> </provider> <!-- ... --> </application> </manifest> PickImageContract を実装します。以下の記事のものとまったく同じものです。 class PickImageContract : ActivityResultContract<Unit, String?>() { companion object { fun canPickImage(context: Context): Boolean { return (createIntentInternal().resolveActivity(context.packageManager) != null) } private fun createIntentInternal(): Intent { return Intent(Intent.ACTION_PICK).setType("image/*") } } override fun createIntent(context: Context, input: Unit): Intent { return createIntentInternal() } override fun parseResult(resultCode: Int, intent: Intent?): String? { return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null } } TakePictureContract を実装します。以下の記事のものとほぼ同じものですが、カメラアプリが選択されなかった場合に空のファイルを削除する cleanOutputFile() を追加しています。 class TakePictureContract( owner: SavedStateRegistryOwner, savedStateKey: String ) : ActivityResultContract<String, String?>() { private var cameraOutputFile: File? = null init { owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_CREATE) { val registry = owner.savedStateRegistry registry.registerSavedStateProvider(savedStateKey) { bundleOf("output" to cameraOutputFile?.toString()) } registry.consumeRestoredStateForKey(savedStateKey)?.getString("output")?.let { output -> cameraOutputFile = File(output) } } }) } override fun createIntent(context: Context, input: String): Intent { val file = File( // /data/user/0/.../files のようなアプリ固有の内部ストレージ context.filesDir, input ).also { cameraOutputFile = it } file.createNewFile() // uri: content://{packageName}.fileprovider/files/Pictures/{input} のような Content URI val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } fun cleanOutputFile() { cameraOutputFile?.let { file -> if (file.length() == 0L) { file.delete() } } cameraOutputFile = null } } ImageChooserContract を実装します。GetContent と TakePictureContract を用いて、どちらも動作するように実装しています。 class ImageChooserContract( owner: SavedStateRegistryOwner, savedStateKey: String ) : ActivityResultContract<Unit, String?>() { private val getContentContract = ActivityResultContracts.GetContent() private val pickImageContract = PickImageContract() private val takePictureContract = TakePictureContract(owner, savedStateKey) override fun createIntent(context: Context, input: Unit): Intent { val canPickImage = PickImageContract.canPickImage(context) val hasCameraApp = (Intent(MediaStore.ACTION_IMAGE_CAPTURE) .resolveActivity(context.packageManager) != null) val getContentIntent = getContentContract.createIntent(context, "image/*") val pickImageIntent = if (canPickImage) { pickImageContract.createIntent(context, Unit) } else null val cameraIntent = if (hasCameraApp) { takePictureContract.createIntent(context, "image.jpeg") } else null return Intent.createChooser(getContentIntent, "画像を選択").apply { val extra = listOfNotNull(pickImageIntent, cameraIntent) if (extra.isNotEmpty()) { putExtra(Intent.EXTRA_INITIAL_INTENTS, extra.toTypedArray()) } } } override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { // result = content://com.android.providers.media.documents/document/image... // result = content://com.google.android.apps.photos.contentprovider/... // result = content://media/external_primary/images/media/... // のような画像 Content URI result = getContentContract.parseResult(resultCode, intent)?.toString() ?: pickImageContract.parseResult(resultCode, intent) ?: takePictureContract.parseResult(resultCode, intent) } takePictureContract.cleanOutputFile() return result } } ImageChooserContract を使用して画像選択か Google Photos アプリかカメラアプリでの写真撮影を実行します。 class MyFragment: Fragment() { private val imageChooserLauncher = registerForActivityResult( // savedStateKey: 他の savedStateKey と被らなければ OK ImageChooserContract(this, "imageChooser") ) { file -> if (file != null) { // file = content://com.android.providers.media.documents/document/image... // file = content://com.google.android.apps.photos.contentprovider/... // file = content://media/external_primary/images/media/... // のような選択した画像または撮影した画像 } else { // 画像選択がキャンセルされた } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = /* ... */ binding.button.setOnClickListener { imageChooserLauncher.launch(Unit) } } // ... 解説 ImageChooserContract では GetContent() と PickImageContract() と TakePictureContract() を初期化して保持しておきます。 PickImageContract.canPickImage() では、ACTION_PICK と mimeType = "image/*" 指定の Intent を resolveActivity() することで Google Photos アプリが端末にインストールされているかを確認しています。 ACTION_IMAGE_CAPTURE Intent の resolveActivity() により、カメラアプリが端末にインストールされているかを確認しています。 Android 11 以上では resolveActivity() の使用にあたり Package Visibility に従う必要があるため、AndroidManifest.xml へ <queries> を宣言しています。 createIntent() で、getContentContract.createIntent() と pickImageContract.createIntent() takePictureContract.createIntent() によりそれぞれの Intent を生成します。 それぞれの Intent を createChooser() でまとめます。このとき、いずれかの Intent をメインの Intent とする必要があります。pickImageIntent と cameraIntent は対象のアプリが存在しなければスキップするため、今回は getContentIntent をメインの Intent に設定しています。それ以外の Intent は EXTRA_INITIAL_INTENTS として設定することで、追加の選択肢に設定します。 parseResult() ではそれぞれの Intent の結果を処理します。ユーザーが画像を選択したのか、カメラアプリで写真を撮影したのかが不明であるため、順に処理する必要があります。今回は GetContent.parseResult()、ickImageContract.parseResult()、TakePictureContract.parseResult() の順で処理しています。 parseResult() を呼び出す順番は、それぞれの Intent の結果の扱いを考慮して決めます。 GetContent Contract は ACTION_GET_CONTENT Intent を発行します。 GetContent.parseResult() の内部実装は以下のとおりとなっています。 public final Uri parseResult(int resultCode, @Nullable Intent intent) { if (intent == null || resultCode != Activity.RESULT_OK) return null; return intent.getData(); } PickImageContract は ACTION_PICK Intent を発行し、parseResult() は以下のとおりとなっています。 override fun parseResult(resultCode: Int, intent: Intent?): String? { return if (resultCode == Activity.RESULT_OK) intent?.data?.toString() else null } TakePictureContract は ACTION_IMAGE_CAPTURE Intent を発行し、parseResult() は以下のとおりとなっています。 override fun parseResult(resultCode: Int, intent: Intent?): String? { var result: String? = null if (resultCode == Activity.RESULT_OK) { result = cameraOutputFile?.toString() } else { cameraOutputFile?.delete() } cameraOutputFile = null return result } GetContent Contract と PickImageContract は resultCode == RESULT_OK かつ、Intent.data != null であれば成功となり、選択した画像のパスが手に入ります。TakePictureContract は resultCode == RESULT_OK であれば成功とみなして、あらかじめ記憶していた cameraOutputFile が撮影した写真の画像ファイルであるとみなしています。 TakePictureContract の条件が緩いため、TakePictureContract.parseResult() を先に処理してしまうと常にカメラアプリで写真撮影をしたと判定されてしまいます。 複数の ActivityResultContract を組み合わせるときは parseResult() の順番に気をつけてください。 PickImageContract.parseResult() は GetContentContract.parseResult() と同じ処理であるため、どちらかの parseResult() は省略しても構いません。Android 標準の画像選択アプリが使われても、Google Photos アプリが使われても、選択された画像のパスは Intent.data に保持されていることに変わりはありません。 parseResult() で結果を取得したあとに takePictureContract.cleanOutputFile() でカメラアプリ向け一時ファイルの削除をしています。cleanOutputFile() ではカメラアプリ向けに作成したファイルのファイルサイズを確認し、0 bytes であればカメラアプリは選択されなかったと判断してファイルを削除しています。カメラアプリで写真を撮影していた場合はファイルサイズが 0 bytes ではないため、ファイルを削除しません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android12対応でexported属性を設定したのに怒られた時の対処法

Android12から、セキュリティ面の改善を目的として、インテントフィルタを使用するアクティビティやサービスなどのコンポーネントでandroid:exported属性を明示的に宣言する必要があります。 しかし私の手元では、インテントフィルタを使用する全てのコンポーネントでandroid:exported属性を宣言したにも関わらず「exported属性を指定してくれ」とエラーになってコンパイルできない事象がありました。 そこでこの記事ではどのようにしてその事象を解決したかを紹介します。 結論 自分で用意したManifestではexported属性をちゃんと宣言してても、依存ライブラリでのManifestで条件を満たしていない場合、最終的にmergeされたManifest内で条件を満たさないことになるため、コンパイルエラーになります。 条件を満たしていないAndroidManifestを持つライブラリやモジュールを探し、(外部ライブラリの場合は)exported属性を宣言されてるバージョンにアップデートすることで解決できるかと思います。 前提条件 Android12 SDKのセットアップが完了している 自分で定義したAndroidManifestファイル内で、インテントフィルタを使用する全てのコンポーネントでandroid:exported属性を宣言している エラー内容 まず、コンパイルできなかった時のエラー内容は以下です。 Manifest merger failed with multiple errors, see log Error: Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined 「Android12以上をターゲットにするときは、インテントフィルターが定義されてるコンポーネントにはandroid:exportedを明示してな」と怒られています。 なぜエラーになるか でも、どんなに辛抱強くAndroidManifest内を見ても、インテントフィルターを定義してるコンポーネントでは、やっぱり全部exportedを宣言してるな。。。なぜだ。。。 とここで、先ほどのエラーログをもう一度見ると、Manifest merger failed with multiple errorsとあります。 そういえばそうだ、AndroidManifestは一つではなく、ライブラリの中にも含まれているんだった。 ドキュメントにはこうあります。 APK ファイルに含めることのできる AndroidManifest.xml ファイルは 1 つだけに限られますが、Android Studio プロジェクト内には、メイン ソースセットや、ビルド バリアント、インポート ライブラリによって提供される複数のマニフェスト ファイルを含めることができます。そのため、アプリをビルドする際、Gradle ビルドは、すべてのマニフェスト ファイルを単一のマニフェスト ファイルにマージし、APK 内にパッケージ化します。 なので、最終的にマージされたAndroidManifestを見てみます。 Merged Manifestを見る メインモジュール(appなど)のAndroidManifest.xmlを開くと、実は下にMerged Manifestというタブがあり、そこからプロジェクト内の、依存ライブラリも含んだ全てのAndroidManifest.xmlをマージしたManifestを見ることができます。 この統合されたManifestファイルを改めて眺めていくと。。。 おっ、この3つのactivityコンポーネントで、インテントフィルターが定義されてるのにexportedの宣言がないことが分かります。 原因はここにあった!! で、こいつらのnameを見てみると、androidx.test:coreのライブラリであることが分かります。 依存ライブラリをアップデートする Android Code Searchを使ってandroidx.test:coreのライブラリのアーティファクトを遡っていくと、androidx.test:core:1.3.1-alpha01から、Manifestの各コンポーネントでexported属性が宣言されてるのが分かります。 もし私のプロジェクトでandroidx.test:coreの依存関係をbuild.gradleに宣言していたら話は早かったのですが、残念なことに私のプロジェクトではこのライブラリは直接宣言しておらず、なんのライブラリが間接的にこのtest:coreに依存しているのかを探さなければなりません。 そこで、テスト関連の他のライブラリが何に依存しているのかをmavenやAndroidX Techを使って一個一個確認していったところ、 androidx.test.ext:junit:1.1.1 が androidx.test:core:1.2.1に依存している androidx.test.espresso:espresso-intents:3.1.1 が androidx.test:core:1.1.0に依存している androidx.fragment:fragment-testing:1.3.3 が androidx.test:core:1.3.0に依存している ということが分かり、これらを、不要なものは削除しつつ、androidx.test:core:1.4.0(1.3.1-alpha01以降)に依存しているバージョンまで上げることで解消することができました。 なお、私のプロジェクトでは、これ以外にも、 com.squareup.leakcanary:leakcanary-android:1.5.1もManifestFileでexported属性が宣言されておらず、バージョンアップをしました。 結論 自分で用意したManifestではexported属性をちゃんと宣言してても、依存ライブラリでのManifestで条件を満たしていない場合、最終的にmergeされたManifest内で条件を満たさないことになるため、コンパイルエラーになります。 条件を満たしていないAndroidManifestを持つライブラリやモジュールを探し、(外部ライブラリの場合は)exported属性を宣言されてるバージョンにアップデートすることで解決できるかと思います。 あと、今回の場合、ライブラリを定期的に更新していれば、こんな色々調べる羽目にはならなかったと思うので、ライブラリは定期的に更新しましょう!! 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む