20210429のAndroidに関する記事は5件です。

[Android]RoomのRewriteQueriesToDropUnusedColumnsアノテーションについて

Room の 2.3.0 から RewriteQueriesToDropUnusedColumns というアノテーションが追加されていたので、こちらについて調べてみました。 Room では以下のように Dao で @Query メソッドを定義することによって、データベースからデータが取得できます。 以下はユーザーのデータを取得する例です。 @Entity(tableName = "users") data class User( @PrimaryKey val id: Int, val name: String, val age: Int, val gender: Gender, val address: String ) @Dao interface UserDao { @Query("SELECT * FROM users") fun getUsers(): List<User> } この時、例えばユーザーの名前と年齢だけあればよいのであれば、以下のように書くこともできます。 data class UserNameAndAge(val name: String, val age: Int) @Dao interface UserDao { @Query("SELECT * FROM users") fun getUsers(): List<UserNameAndAge> // 戻り値を List<User> から List<UserNameAndAge> に変更 } ただ、この状態でビルドを実行すると、コンパイラが以下のような警告を出力します。 warning: The query returns some columns [id, address, gender] which are not used by UserNameAndAge. クエリでは id, address, gender のカラムを返しているのに、UserNameAndAge では使われていないと警告しています。 続けて警告を読んでいくと、以下のような文が出力されています。 You can annotate the method with @RewriteQueriesToDropUnusedColumns to direct Room to rewrite your query to avoid fetching unused columns. どうやらメソッドに @RewriteQueriesToDropUnusedColumns アノテーションを付けると、Room は不必要なカラムを取得しないようにクエリを書き換えてくれるようです。 まずはこのアノテーションが付いていない状態でコンパイルした際に生成される UserDao_Impl.java を見ると以下のようにコードが生成されていました。 UserDao_Impl.java @Override public List<UserNameAndAge> getUsers() { final String _sql = "SELECT * FROM users"; ... } 次に、以下のようにアノテーションを付与してコンパイルしてみます。 @Dao interface UserDao { @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM users") fun getUsers(): List<UserNameAndAge> } この状態でコンパイルして生成された UserDao_Impl.java は以下です。 UserDao_Impl.java @Override public List<UserNameAndAge> getUsers() { final String _sql = "SELECT `name`, `age` FROM (SELECT * FROM users)"; ... } 確かにクエリが書き換えられているようです。 ドキュメント を読む限りだと、このクエリは Sqlite 側では SELECT name, age FROM users と変更してくれるようです。 また、@RewriteQueriesToDropUnusedColumns アノテーションはメソッドだけではなく @Dao のインターフェース(もしくはクラス)や @Database のクラスにも付与することができ、適用する範囲を Dao 単位や Database 単位にすることもできるようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidQ以上でmockito-androidを使ったInstrumentationTestが動かない

現象 Instrumentation TestでMockitoをKotlinで使うため、以下のようにライブラリを設定して使っていました。 app/build.gradle androidTestImplementation 'org.mockito:mockito-core:3.9.0' androidTestImplementation ('org.mockito.kotlin:mockito-kotlin:3.1.0'){ exclude group: 'org.mockito', module: 'mockito-core' } androidTestImplementation ('com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.1'){ exclude group: 'org.mockito', module: 'mockito-core' } ところが、P(Android9)の端末(エミュレーター)では問題なく動作するのに、Q以上(Android10)以上の端末orエミュレーターで動かそうとするとモックに失敗してしまいました。 エラーメッセージ java.lang.NullPointerException: Attempt to invoke interface method 'boolean org.mockito.plugins.MockMaker$TypeMockability.mockable()' on a null object reference at org.mockito.internal.util.MockCreationValidator.validateType(MockCreationValidator.java:23) at org.mockito.internal.creation.MockSettingsImpl.validatedSettings(MockSettingsImpl.java:250) at org.mockito.internal.creation.MockSettingsImpl.build(MockSettingsImpl.java:232) at org.mockito.internal.MockitoCore.mock(MockitoCore.java:61) at org.mockito.Mockito.mock(Mockito.java:1951) at org.mockito.Mockito.mock(Mockito.java:1862) 解決方法 どうやら64bitライブラリが問題?なようで、以下の設定を行うと解消しました。 androidTest用のマニフェストファイルを作成する androidTest下に、AndroidManifest.xmlを作成し、以下のように記述します。 androidTest/AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.package"> <application android:extractNativeLibs="true" /> </manifest> これだけで、テストが通るようになりました。 解決方法に辿り着いた経緯 mockito-kotlinが使えないなら、MockKを使ってみようと思い変更したのですが、やはり同じで、Android Pでは動いて、Q以上では動かない。 ただしエラーメッセージは異なっていました。 Caused by: io.mockk.proxy.MockKAgentException: MockK could not self-attach a jvmti agent to the current VM. This feature is required for inline mocking. This error occured due to an I/O error during the creation of this agent: java.io.IOException: No such file or directory Potentially, the current VM does not support the jvmti API correctly at io.mockk.proxy.android.AndroidMockKAgentFactory.init(AndroidMockKAgentFactory.kt:63) at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:46) at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:172) このメッセージでググって、辿り着いたのが以下のissue. ここで64bit向けのライブラリがapkにないせいではないかと指摘されており、コメントで提案されていた案の1つを試してみたところ上手くいきました。 そこで、mockito-kotlinに戻してみて同じように設定し、テストを動かしてみたら無事に動いた、というわけです。 android:extractNativeLibsについて こちらのページによれば、android:extractNativeLibsはデフォルトでtrueらしいのですが、Instrumentationテスト実施時には違う、ということでしょうか。。。 (でも、実機は説明付くんだけど、エミュレーターのimageはx86で、x86_64じゃないんだけどな・・・・動かしているマシンが64bitだからそちらに引っ張られるんだろうか?)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidQ以上でmockito-kotlinを使ったInstrumentationTestが動かない

現象 Instrumentation TestでMockitoをKotlinで使うため、以下のようにライブラリを設定して使っていました。 app/build.gradle androidTestImplementation 'org.mockito:mockito-core:3.9.0' androidTestImplementation ('org.mockito.kotlin:mockito-kotlin:3.1.0'){ exclude group: 'org.mockito', module: 'mockito-core' } androidTestImplementation ('com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.1'){ exclude group: 'org.mockito', module: 'mockito-core' } ところが、P(Android9)の端末(エミュレーター)では問題なく動作するのに、Q以上(Android10)以上の端末orエミュレーターで動かそうとするとモックに失敗してしまいました。 エラーメッセージ java.lang.NullPointerException: Attempt to invoke interface method 'boolean org.mockito.plugins.MockMaker$TypeMockability.mockable()' on a null object reference at org.mockito.internal.util.MockCreationValidator.validateType(MockCreationValidator.java:23) at org.mockito.internal.creation.MockSettingsImpl.validatedSettings(MockSettingsImpl.java:250) at org.mockito.internal.creation.MockSettingsImpl.build(MockSettingsImpl.java:232) at org.mockito.internal.MockitoCore.mock(MockitoCore.java:61) at org.mockito.Mockito.mock(Mockito.java:1951) at org.mockito.Mockito.mock(Mockito.java:1862) 解決方法 どうやら64bitライブラリが問題?なようで、以下の設定を行うと解消しました。 androidTest用のマニフェストファイルを作成する androidTest下に、AndroidManifest.xmlを作成し、以下のように記述します。 androidTest/AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.package"> <application android:extractNativeLibs="true" /> </manifest> これだけで、テストが通るようになりました。 解決方法に辿り着いた経緯 mockito-kotlinが使えないなら、MockKを使ってみようと思い変更したのですが、やはり同じで、Android Pでは動いて、Q以上では動かない。 ただしエラーメッセージは異なっていました。 Caused by: io.mockk.proxy.MockKAgentException: MockK could not self-attach a jvmti agent to the current VM. This feature is required for inline mocking. This error occured due to an I/O error during the creation of this agent: java.io.IOException: No such file or directory Potentially, the current VM does not support the jvmti API correctly at io.mockk.proxy.android.AndroidMockKAgentFactory.init(AndroidMockKAgentFactory.kt:63) at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:46) at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:172) このメッセージでググって、辿り着いたのが以下のissue. ここで64bit向けのライブラリがapkにないせいではないかと指摘されており、コメントで提案されていた案の1つを試してみたところ上手くいきました。 そこで、mockito-kotlinに戻してみて同じように設定し、テストを動かしてみたら無事に動いた、というわけです。 android:extractNativeLibsについて こちらのページによれば、android:extractNativeLibsはデフォルトでtrueらしいのですが、Instrumentationテスト実施時には違う、ということでしょうか。。。 (でも、実機は説明付くんだけど、エミュレーターのimageはx86で、x86_64じゃないんだけどな・・・・動かしているマシンが64bitだからそちらに引っ張られるんだろうか?) 追記 AGP(Android Gradle Plguin)3.6.0から、デフォルトはfalseに変わったようです。 mockito-kotlinとMockKを同時に使う方法 MockKに置き換える際、いっぺんに変えるのは無理だったので単純なテストだけ置き換えたのですが、その際、mockito-kotlinとMockKに単純に同時に依存するとテストが実行出来ませんでした。 (エラーメッセージは忘れてしまいましたが、ビルドの時点で失敗したと思います) 以下のサイトが参考になりました。 https://medium.com/monsterculture/how-to-use-mockito-together-with-mockk-in-android-instrumentation-tests-c8d022fac04e
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 10(API30)にしたら画像関係処理おかしくなくなったので対応した。

この記事に関して OSアップデートしたら、自分が開発して自分で使ってるAndroidアプリにおいて、画像回りの動作がおかしくなってしまいました。動作を要約すると、他の画像を読み込んで、決まった加工を施して別画像に保存するアプリです。 ※GooglePlayで公開はしているものの、ほぼ自分用のアプリ その対応に四苦八苦したので、他の皆さんの参考になればと思いポイントをまとめたものになります。 Android Virtual Device で API30(Android 11)起動する時のトラブル エラー確認の為には当然エミュレーターで動作確認しますが、起動すると The emulator process for AVD 5.1_WVGA_API_30 was killed. とかのエラーが出てしまってました。 散々再インストールとかしたが改善せず。結局以下の投稿を参考にして、EmulatorをSoftwareにしたら起動できるようになりました。 Android API 30 (Android 11.0 / Android R) keeps/always getting killed 他には、必要なモジュールが無かったり、ディスク容量足りなかったりするケースもある模様です。詳細は以下動画で。こちらはAPI28以下でも発生する内容だと思うので、その症状だった場合にはこちらを参考にするといいかも。 Android API 30 (Android 11.0 / Android R) keeps/always getting killed ファイルパスで画像を取得しようとしていた時のエラー E/BitmapFactory: Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/Pictures/IMG_20210424_030736.jpg: open failed: EACCES (Permission denied) java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:800) /storage/emulated/0/Pictures/IMG_20210424_030736.jpg というのは、画像選択してから、uri.getPath().toString(); で取得した情報を加工して取得したファイルシステム上パスです。 該当処理 bmp=BitmapFactory.decodeFile(baseImgfilePath ,options); 公式ページ - DATAカラムに関する記述 を見ると、API29からアプリケーションは物理パスを使っての直接アクセスは許可されなくなったという事ですね。 This constant was deprecated in API level 29. Apps may not have filesystem permissions to directly access this path. Instead of trying to open this path directly, apps should use ContentResolver#openFileDescriptor(Uri, String) to gain access. だからPermissionDeniedになってしまった様です。ContentResolver#openFileDescriptor(Uri, String)を使って取得すべきという事ですね。 Uriは都度変わってしまう ファイルシステム上パスが使えないならUriを保存しておいてそれを使えばよいと考えましたが、そうではない様です。ギャラリーなどで情報を取得すると思いますが、その結果として取得できるUriは実行の度に変更されてしまいます。一回取得したUriは、自分の想像ですが、Activityが変わったりすると使用不可能になる様です。イメージ的にはWebページで表示した時に埋め込まれるトークンで二重実行とかの不正を防いでいる感じでしょうか。 ギャラリー等で画像選択する処理 // ・・中略・・ // 選択処理開始 public void setBasePicPath(View v) { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, ACTIVITY_BASEPICSEL); // ・・中略・・ // 選択後の処理 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(resultCode == RESULT_OK){ switch(requestCode){ case ACTIVITY_BASEPICSEL: System.out.println(data.getData().toString()); break; // ・・中略・・ 2回同じ画像を選択した結果 content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F15/ORIGINAL/NONE/image%2Fjpeg/983691334 content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F15/ORIGINAL/NONE/image%2Fjpeg/1264091695 保存先は固定して、ファイル名で情報を管理 公式ページ - データ ストレージとファイル ストレージの概要 によると画像はメディアに属すると思います。アプリ固有のファイルとする事も出来そうですが、特にこだわりないので扱いやすそうなメディアファイルとして扱う事にしました。 公式ページ - ファイルの場所に関するヒントを提供する を読むと、特定のフォルダ以下の相対フォルダ内に画像を出するという事は出来るという事で、自分のアプリ用のフォルダを作成してそこで管理するのがよさそうです。 実際のアクセスにはUriが必要だけど、UriはDBなどに保存して使いまわせる情報ではない(と思う)ので、ファイル名で判断する様な感じです。IDは今の所不変っぽいですが、怖いので今のうちにファイル名判断にします。 自分はこんな感じで、ファイル名から情報取得する関数を作っておきました。ImageInfoStruct は自作クラスです。 ファイル名(displayName)からUriを取得する処理 public static ImageInfoStruct getImageInfo(ContentResolver contentResolver, String displayName) { ImageInfoStruct retInfo = new ImageInfoStruct(); String[] selectionArgs = new String[]{displayName}; try (Cursor cursor = contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_INFO_COLUMNS, MediaStore.Images.Media.DISPLAY_NAME + "=?", selectionArgs, null)) { int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME); if (cursor.moveToNext()) { retInfo.id = cursor.getInt(idColumn); retInfo.displayName = cursor.getString(nameColumn); retInfo.uri = ContentUris.withAppendedId(targetUri, retInfo.id);; return retInfo; } else { return null; } } } ※同名ファイルがあると問題になるはずです。ただ、保存時同名ファイルがあると(1)とか自動で付くっぽいので実際には問題にならないのかも?Uriをいじくれば前述RelativePathで絞れるのかもしれないけど、今のところやり方を見つける事が出来ませんでした。変に上書きするのが怖いので、今の所はアプリストアに公開はせずに、後にチェック処理追加する予定です。 メディアファイルの更新フロー 自分がアプリを作成した時(Android4.1の頃)は、画像保存した後に他のアプリで認識できるようにするためにはメディアファイル情報をリフレッシュする必要があったと思います(当時の知識不足だけかも)。スレッド作成してその中で処理したりして結構面倒でした。今は、自分でその情報を更新する必要がありますが、反対に不要なリフレッシュは要らなくなり、結果処理が早くなったようです。具体的に言うと、以下の感じです。 MediaStore.Images.Media.IS_PENDING ⁼ true のContentValuesを作成する そのContentValuesを使って、contentResolver.insert にてメディア情報用のレコードを作成し、Uriを取得。 その時のメディアファイルの保存先はシステムに依存する。MediaStore.Images.Media.EXTERNAL_CONTENT_URIを使用。 contentResolver.openFileDescriptor で FileDescriptorを取得。 FileOutputStream(FileInputStream) を取得してファイル処理 処理が終わったら、MediaStore.Images.Media.IS_PENDING ⁼ false のContentValuesを作成する そのContentValuesを使って、contentResolver.update にてメディア情報用のレコードを更新する。 メディア(画像)情報の更新 ContentValues newImageDetails = new ContentValues(); newImageDetails.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/BaseImage/"); newImageDetails.put(MediaStore.Images.Media.DISPLAY_NAME, srcFileName); newImageDetails.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); newImageDetails.put(MediaStore.Images.Media.TITLE, title); newImageDetails.put(MediaStore.Images.Media.IS_PENDING, true); Uri destImageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, newImageDetails); try ( ParcelFileDescriptor destPfd = contentResolver.openFileDescriptor(destImageUri, "w"); FileOutputStream destFos = new FileOutputStream(destPfd.getFileDescriptor()); ) { // destFosに対する処理を行う部分 } catch (IOException ex) { ex.printStackTrace(); } finally { ContentValues updImageDetails = new ContentValues(); updImageDetails.put(MediaStore.Images.Media.IS_PENDING, false); contentResolver.update(destImageUri, updImageDetails, null, null); } おまけ:サムネイル対応 自分がアプリを作成した時、リスト表示用のサムネイルは自分で加工していましたが、今はその機能が標準で準備されている様子です(当時の知識不足だけかも)。 公式ページ - ファイルのサムネイルを読み込む おまけ:DBのアップグレード 今回、ファイル名で情報を管理する事にしたので、そのカラム追加です。 新規インストールではcreate文側で新カラムが生成されるけど、既に使ってる場合にはonCreate処理が実行されないので、onUpgradeで処理を追加する必要があります。 ※Create文にはカラム追加せず、onCreateで新カラム追加処理をする方向性もアリだと思うけど今回はこちらを使用。 DBのバージョンを指定 SQLiteOpenHelper継承クラス public class StickyDbHelper extends SQLiteOpenHelper { // ・・中略・・ private static final int VERSION = 2; // ・・中略・・ public StickyDbHelper(Context context) { super(context, DBNAME, FACTORY, VERSION); } // ・・後略・・ Upgrade処理追加 古いバージョンと新しいバージョンの境目で処理されるようにしています。単一バージョン指定だと、バージョン飛びでアップグレードするケースに対応できないです。 SQLiteOpenHelper継承クラス public class HogehogeDbHelper extends SQLiteOpenHelper { // ・・中略・・ private static final String SQL_UPGRADE_1_2_BASE_IMAGE_URI = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_BASE_IMAGE_URI + " text"; private static final String SQL_UPGRADE_1_2_OUT_IMAGE_DEST_TYPE = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_OUT_IMAGE_DEST_TYPE + " text"; private static final String SQL_UPGRADE_1_2_OUT_IMAGE_RELATIVE_PATH = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_OUT_IMAGE_RELATIVE_PATH + " text"; // ・・中略・・ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion <= 1 && newVersion >= 2){ db.beginTransaction(); try { // add COLUMN_BASE_IMAGE_URI, COLUMN_OUT_IMAGE_DEST_TYPE, COLUMN_OUT_IMAGE_RELATIVE_PATH db.execSQL(SQL_UPGRADE_1_2_BASE_IMAGE_URI); db.execSQL(SQL_UPGRADE_1_2_OUT_IMAGE_DEST_TYPE); db.execSQL(SQL_UPGRADE_1_2_OUT_IMAGE_RELATIVE_PATH); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } // ・・後略・・ そのままだとアップグレードのテストは1回しかできないので、古い状況でエミュレーターのバックアップとか取っておいた方が良いと思います。 参考にさせて頂いたサイト Android 公式 アプリの権限をリクエストする データ ストレージとファイル ストレージの概要 共有ストレージからメディア ファイルにアクセスする ファイルのサムネイルを読み込む stack overflow Android API 30 (Android 11.0 / Android R) keeps/always getting killed How to Fix Exception ‘open failed: EACCES (Permission denied)’ on Android YouTube The Emulator process for AVD was killed Android Studio : 4 solutions 個人ブログ [Android] Storage Access Framework でフォトアプリから画像を取り出す Android Qからの画像保存 アンドロイド - MediaStoreにメディアファイルを保存する方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 11(API30)にしたら画像関係処理おかしくなくなったので対応した。

この記事に関して OSアップデートしたら、自分が開発して自分で使ってるAndroidアプリにおいて、画像回りの動作がおかしくなってしまいました。動作を要約すると、他の画像を読み込んで、決まった加工を施して別画像に保存するアプリです。 ※GooglePlayで公開はしているものの、ほぼ自分用のアプリ その対応に四苦八苦したので、他の皆さんの参考になればと思いポイントをまとめたものになります。 Android Virtual Device で Android 11(API30)起動する時のトラブル エラー確認の為には当然エミュレーターで動作確認しますが、起動すると The emulator process for AVD 5.1_WVGA_API_30 was killed. とかのエラーが出てしまってました。 散々再インストールとかしたが改善せず。結局以下の投稿を参考にして、EmulatorをSoftwareにしたら起動できるようになりました。 Android API 30 (Android 11.0 / Android R) keeps/always getting killed 他には、必要なモジュールが無かったり、ディスク容量足りなかったりするケースもある模様です。詳細は以下動画で。こちらはAPI28以下でも発生する内容だと思うので、その症状だった場合にはこちらを参考にするといいかも。 Android API 30 (Android 11.0 / Android R) keeps/always getting killed ファイルパスで画像を取得しようとしていた時のエラー E/BitmapFactory: Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/Pictures/IMG_20210424_030736.jpg: open failed: EACCES (Permission denied) java.lang.NullPointerException: Attempt to invoke virtual method 'int android.graphics.Bitmap.getWidth()' on a null object reference at android.graphics.Bitmap.createScaledBitmap(Bitmap.java:800) /storage/emulated/0/Pictures/IMG_20210424_030736.jpg というのは、画像選択してから、uri.getPath().toString(); で取得した情報を加工して取得したファイルシステム上パスです。 該当処理 bmp=BitmapFactory.decodeFile(baseImgfilePath ,options); 公式ページ - DATAカラムに関する記述 を見ると、API29からアプリケーションは物理パスを使っての直接アクセスは許可されなくなったという事ですね。 This constant was deprecated in API level 29. Apps may not have filesystem permissions to directly access this path. Instead of trying to open this path directly, apps should use ContentResolver#openFileDescriptor(Uri, String) to gain access. だからPermissionDeniedになってしまった様です。ContentResolver#openFileDescriptor(Uri, String)を使って取得すべきという事ですね。 Uriは都度変わってしまう ファイルシステム上パスが使えないならUriを保存しておいてそれを使えばよいと考えましたが、そうではない様です。ギャラリーなどで情報を取得すると思いますが、その結果として取得できるUriは実行の度に変更されてしまいます。一回取得したUriは、自分の想像ですが、Activityが変わったりすると使用不可能になる様です。イメージ的にはWebページで表示した時に埋め込まれるトークンで二重実行とかの不正を防いでいる感じでしょうか。 ギャラリー等で画像選択する処理 // ・・中略・・ // 選択処理開始 public void setBasePicPath(View v) { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, ACTIVITY_BASEPICSEL); // ・・中略・・ // 選択後の処理 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if(resultCode == RESULT_OK){ switch(requestCode){ case ACTIVITY_BASEPICSEL: System.out.println(data.getData().toString()); break; // ・・中略・・ 2回同じ画像を選択した結果 content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F15/ORIGINAL/NONE/image%2Fjpeg/983691334 content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F15/ORIGINAL/NONE/image%2Fjpeg/1264091695 保存先は固定して、ファイル名で情報を管理 公式ページ - データ ストレージとファイル ストレージの概要 によると画像はメディアに属すると思います。アプリ固有のファイルとする事も出来そうですが、特にこだわりないので扱いやすそうなメディアファイルとして扱う事にしました。 公式ページ - ファイルの場所に関するヒントを提供する を読むと、特定のフォルダ以下の相対フォルダ内に画像を出するという事は出来るという事で、自分のアプリ用のフォルダを作成してそこで管理するのがよさそうです。 実際のアクセスにはUriが必要だけど、UriはDBなどに保存して使いまわせる情報ではない(と思う)ので、ファイル名で判断する様な感じです。IDは今の所不変っぽいですが、怖いので今のうちにファイル名判断にします。 自分はこんな感じで、ファイル名から情報取得する関数を作っておきました。ImageInfoStruct は自作クラスです。 ファイル名(displayName)からUriを取得する処理 public static ImageInfoStruct getImageInfo(ContentResolver contentResolver, String displayName) { ImageInfoStruct retInfo = new ImageInfoStruct(); String[] selectionArgs = new String[]{displayName}; try (Cursor cursor = contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_INFO_COLUMNS, MediaStore.Images.Media.DISPLAY_NAME + "=?", selectionArgs, null)) { int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME); if (cursor.moveToNext()) { retInfo.id = cursor.getInt(idColumn); retInfo.displayName = cursor.getString(nameColumn); retInfo.uri = ContentUris.withAppendedId(targetUri, retInfo.id);; return retInfo; } else { return null; } } } ※同名ファイルがあると問題になるはずです。ただ、保存時同名ファイルがあると(1)とか自動で付くっぽいので実際には問題にならないのかも?Uriをいじくれば前述RelativePathで絞れるのかもしれないけど、今のところやり方を見つける事が出来ませんでした。変に上書きするのが怖いので、今の所はアプリストアに公開はせずに、後にチェック処理追加する予定です。 メディアファイルの更新フロー 自分がアプリを作成した時(Android4.1の頃)は、画像保存した後に他のアプリで認識できるようにするためにはメディアファイル情報をリフレッシュする必要があったと思います(当時の知識不足だけかも)。スレッド作成してその中で処理したりして結構面倒でした。今は、自分でその情報を更新する必要がありますが、反対に不要なリフレッシュは要らなくなり、結果処理が早くなったようです。具体的に言うと、以下の感じです。 MediaStore.Images.Media.IS_PENDING ⁼ true のContentValuesを作成する そのContentValuesを使って、contentResolver.insert にてメディア情報用のレコードを作成し、Uriを取得。 その時のメディアファイルの保存先はシステムに依存する。MediaStore.Images.Media.EXTERNAL_CONTENT_URIを使用。 contentResolver.openFileDescriptor で FileDescriptorを取得。 FileOutputStream(FileInputStream) を取得してファイル処理 処理が終わったら、MediaStore.Images.Media.IS_PENDING ⁼ false のContentValuesを作成する そのContentValuesを使って、contentResolver.update にてメディア情報用のレコードを更新する。 メディア(画像)情報の更新 ContentValues newImageDetails = new ContentValues(); newImageDetails.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/BaseImage/"); newImageDetails.put(MediaStore.Images.Media.DISPLAY_NAME, srcFileName); newImageDetails.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); newImageDetails.put(MediaStore.Images.Media.TITLE, title); newImageDetails.put(MediaStore.Images.Media.IS_PENDING, true); Uri destImageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, newImageDetails); try ( ParcelFileDescriptor destPfd = contentResolver.openFileDescriptor(destImageUri, "w"); FileOutputStream destFos = new FileOutputStream(destPfd.getFileDescriptor()); ) { // destFosに対する処理を行う部分 } catch (IOException ex) { ex.printStackTrace(); } finally { ContentValues updImageDetails = new ContentValues(); updImageDetails.put(MediaStore.Images.Media.IS_PENDING, false); contentResolver.update(destImageUri, updImageDetails, null, null); } おまけ:サムネイル対応 自分がアプリを作成した時、リスト表示用のサムネイルは自分で加工していましたが、今はその機能が標準で準備されている様子です(当時の知識不足だけかも)。 公式ページ - ファイルのサムネイルを読み込む おまけ:DBのアップグレード 今回、ファイル名で情報を管理する事にしたので、そのカラム追加です。 新規インストールではcreate文側で新カラムが生成されるけど、既に使ってる場合にはonCreate処理が実行されないので、onUpgradeで処理を追加する必要があります。 ※Create文にはカラム追加せず、onCreateで新カラム追加処理をする方向性もアリだと思うけど今回はこちらを使用。 DBのバージョンを指定 SQLiteOpenHelper継承クラス public class StickyDbHelper extends SQLiteOpenHelper { // ・・中略・・ private static final int VERSION = 2; // ・・中略・・ public StickyDbHelper(Context context) { super(context, DBNAME, FACTORY, VERSION); } // ・・後略・・ Upgrade処理追加 古いバージョンと新しいバージョンの境目で処理されるようにしています。単一バージョン指定だと、バージョン飛びでアップグレードするケースに対応できないです。 SQLiteOpenHelper継承クラス public class HogehogeDbHelper extends SQLiteOpenHelper { // ・・中略・・ private static final String SQL_UPGRADE_1_2_BASE_IMAGE_URI = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_BASE_IMAGE_URI + " text"; private static final String SQL_UPGRADE_1_2_OUT_IMAGE_DEST_TYPE = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_OUT_IMAGE_DEST_TYPE + " text"; private static final String SQL_UPGRADE_1_2_OUT_IMAGE_RELATIVE_PATH = "ALTER TABLE " + STICKY_LIST_TABLE + " ADD COLUMN " + COLUMN_OUT_IMAGE_RELATIVE_PATH + " text"; // ・・中略・・ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion <= 1 && newVersion >= 2){ db.beginTransaction(); try { // add COLUMN_BASE_IMAGE_URI, COLUMN_OUT_IMAGE_DEST_TYPE, COLUMN_OUT_IMAGE_RELATIVE_PATH db.execSQL(SQL_UPGRADE_1_2_BASE_IMAGE_URI); db.execSQL(SQL_UPGRADE_1_2_OUT_IMAGE_DEST_TYPE); db.execSQL(SQL_UPGRADE_1_2_OUT_IMAGE_RELATIVE_PATH); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } // ・・後略・・ そのままだとアップグレードのテストは1回しかできないので、古い状況でエミュレーターのバックアップとか取っておいた方が良いと思います。 参考にさせて頂いたサイト Android 公式 アプリの権限をリクエストする データ ストレージとファイル ストレージの概要 共有ストレージからメディア ファイルにアクセスする ファイルのサムネイルを読み込む stack overflow Android API 30 (Android 11.0 / Android R) keeps/always getting killed How to Fix Exception ‘open failed: EACCES (Permission denied)’ on Android YouTube The Emulator process for AVD was killed Android Studio : 4 solutions 個人ブログ [Android] Storage Access Framework でフォトアプリから画像を取り出す Android Qからの画像保存 アンドロイド - MediaStoreにメディアファイルを保存する方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む