Contents
経緯
アルバムアプリのようなものを作ろうとしています。今回調査した機能としては、その中の「画像を選択してパスを取得、パスをデータベースに保存。そのデータベースからパスの一覧等を取得、画像を表示する」機能の部分。RecylcerViewに画像を表示させることを考えています。
SAF(Storage Access Framework)で画像を取り出すのは簡単そうでしたし、実際にやってみると簡単でした。
そこでSAFのURIがMedia Storeを使用する方法でも扱えると安易に考えて、軽いサンプルコードであたりをつけはじめました。(そして、しばらくはまりました。)
画像を取得するピッカをIntentで起動して(SAF にて)、URIを取得して画像化する部分に入力すればOKかとおもっていましたが、画像化する部分でエラーが発生。簡単にはちょちょいと解決できなさそうでしたので、ちょっと本腰を入れて調査することにしました。
調査結果 その1.画像取得に関して
画像を取得するときには、現在2種類方法があります。
- SAF(Storage Access Framework)を使用する方法
- ContentProvider(Media Store)を使用する方法
SAF(Storage Access Framework)を使用する方法の参考URL
ContentProvider(Media Store)を使用する方法の参考URL
- [Android] MediaStore スマホの画像や音楽ファイルを検索する
- [Android] 外部ストレージに画像を保存・読出しをする
- CONTENTPROVIDERで端末内画像データを取得する
調査結果 その2.画像取得方法の相違点に関して
SAFで取得するURIはSAFで使用するための専用のURI
SAFで使用するためのURI content://となっています。
「content://com.android.providers.media.documents/document/XXXXX」の形では、
SAFのシステムの画像選択ウィンドウでの形式であり、パスと異なります。
パスとURIに関しては、「Android 画像ファイルを扱う際のFileとUriまとめ」を参照してもらうと
content://com.android.providers.media.documents/document/コンテンツID …… システムの画像選択ウィンドウ
のように書かれています。SAFでは実際にこのようなURIになっています。
Media Storeのパスの形式
Media Storeのパスの形式は
「/storage/emulated/0/DCIM/Camera/IMG_20190331_100436.jpg」
となっており、 SAFのURI形式からはそのまま直接変換することができません。(文字列操作のみでという意味)
Media Storeのパス取得に関して
Media Storeのパスを取得するためには、パーミッションのコードが必要です。
nyanさんの[Android] MediaStore スマホの画像や音楽ファイルを検索するが参考になりました。
SAFのURIからMedia Storeのパスへの変換方法
SAFのURI→Media Storeのパスは、直接ではありませんが取得する方法があります。
stackoverflowのandroid – cursor didn’t have _data column not foundを参照ください。
実際にやってみた 部分コード編
「パーミッション(onCreate内)→SAF→SAFのURI取得→変換→パス取得」を行います。コードを追加していく元のサンプルとしては、以下を参照ください。
全体のコードは長いので、最後に載せています。
1.下ごしらえ①として、AndroidManifest.xml にusers-permissionの記述を入れます
2.下ごしらえ②として、 onCreate()内にパーミッションの関数を追加。パーミッションの関数はこちらの”[Android] MediaStore スマホの画像や音楽ファイルを検索する” を参照
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) Log.d("MainActivity", "onCreate()") if(Build.VERSION.SDK_INT >= 23){ Log.d("MainActivity", "SDK > 23") checkPermission() } else{ Log.d("MainActivity", "SDK < 22") readContentActivity(); }
3.その他下ごしらえとして、パーミッションの関数を追加しています。②-1~4を参照ください。
4.SAFでURIを取得。
5. コードとしては、onActivityResult()内で、”uri = resultData.data”の部分
6. stackoverflowのandroid – cursor didn’t have _data column not foundの関数をKotlinに変換して使用。パスが取得できました。下ごしらえ③として、コードを見てくだください
var path: String? = getPathFromUri( applicationContext , uri)
7. 変換関数にてパスを取得したので、今回のサンプルではパスの方で画像を表示します。”[Android] 外部ストレージに画像を保存・読出しをする“の読み出し参照。
val file = File(path) val inputStream0 = FileInputStream(file) val bitmap = BitmapFactory.decodeStream(inputStream0) imageView.setImageBitmap(bitmap)
8.変換したものもログに出して確認します。
D/onActivityResult: content://com.android.providers.media.documents/document/image%3A54 D/onActivityResult Path: /storage/emulated/0/DCIM/Camera/IMG_20190331_100436.jpg
9.SAFと同じように画像が表示できていればOKです。
実際にやってみた 画像編
1,実行するとパーミッションが出ます。
2.FAB(Floating Action Button)を押してください。
3.SAFで画像を選択可能になります。選択してください。
4. URIからパスに変換して画像が表示できました。
5. ログでSAFのURIとパスの変換を確認。正しく取得できているようです。
コード kotlin
ちょっと長くなりましたが、Kotlinのコードをのせておきます。参考もとを変換してうまくいかなかったときに参考にしてください。ちょっと長いので、もうちょっと見やすい形や書き方を考えたいと思いました。
import android.Manifest import android.annotation.TargetApi import android.app.Activity import android.content.ContentResolver import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity; import android.util.Log import android.view.Menu import android.view.MenuItem import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.content_main.* import java.io.IOException import android.content.Context import java.io.File import android.provider.DocumentsContract import android.content.ContentUris import android.content.pm.PackageManager import android.database.Cursor import android.graphics.BitmapFactory import android.os.Environment.getExternalStorageDirectory import android.os.Build import android.os.Environment import android.support.v4.app.FragmentActivity import android.support.v4.content.ContextCompat import android.widget.Toast import java.io.FileInputStream import java.io.InputStream // 下ごしらえ① AndroidManifest.xml にusers-permissionの記述を入れてます。 class MainActivity : AppCompatActivity() { private val REQUEST_PERMISSION : Int = 10 private val READ_REQUEST_CODE = 42 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) Log.d("MainActivity", "onCreate()") //下ごしらえ ②-1 パーミッションの追加 if(Build.VERSION.SDK_INT >= 23){ Log.d("MainActivity", "SDK > 23") checkPermission() } else{ Log.d("MainActivity", "SDK < 22") readContentActivity(); } fab.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "image/*" startActivityForResult(intent, READ_REQUEST_CODE) } } public override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { var uri: Uri? if (resultData != null) { uri = resultData.data try { // val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri) // imageView.setImageBitmap(bitmap) Log.d("onActivityResult", uri.toString()) var path: String? = getPathFromUri( applicationContext , uri) //https://akira-watson.com/android/external-storage-image.html val file = File(path) val inputStream0 = FileInputStream(file) val bitmap = BitmapFactory.decodeStream(inputStream0) imageView.setImageBitmap(bitmap) Log.d("onActivityResult Path", path) } catch (e: IOException) { e.printStackTrace() } } } } // 下ごしらえ③-1 変換関数 Uri to path //https://qiita.com/wakamesoba98/items/98b79bdfde19612d12b0 //https://stackoverflow.com/questions/32661221/android-cursor-didnt-have-data-column-not-found/33930169#33930169 fun getPathFromUri(context: Context, uri: Uri): String? { val isAfterKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT // DocumentProvider Log.e("getPathFromUri", "uri:" + uri.authority!!) if (isAfterKitKat && DocumentsContract.isDocumentUri(context, uri)) { if ("com.android.externalstorage.documents" == uri.authority) {// ExternalStorageProvider val docId = DocumentsContract.getDocumentId(uri) val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val type = split[0] if ("primary".equals(type, ignoreCase = true)) { return (Environment.getExternalStorageDirectory().path + "/" + split[1]) } else { return "/stroage/" + type + "/" + split[1] } } else if ("com.android.providers.downloads.documents" == uri.authority) {// DownloadsProvider val id = DocumentsContract.getDocumentId(uri) val contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) ) return getDataColumn(context, contentUri, null, null) } else if ("com.android.providers.media.documents" == uri.authority) {// MediaProvider val docId = DocumentsContract.getDocumentId(uri) val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() var contentUri: Uri? = MediaStore.Files.getContentUri("external") val selection = "_id=?" val selectionArgs = arrayOf(split[1]) return getDataColumn(context, contentUri, selection, selectionArgs) } } else if ("content".equals(uri.scheme!!, ignoreCase = true)) {//MediaStore return getDataColumn(context, uri, null, null) } else if ("file".equals(uri.scheme!!, ignoreCase = true)) {// File return uri.path } return null } // 下ごしらえ③-2 変換関数 fun getDataColumn( context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>? ): String? { var cursor: Cursor? = null val projection = arrayOf(MediaStore.Files.FileColumns.DATA) try { cursor = context.contentResolver.query( uri!!, projection, selection, selectionArgs, null ) if (cursor != null && cursor.moveToFirst()) { val cindex = cursor.getColumnIndexOrThrow(projection[0]) return cursor.getString(cindex) } } finally { if (cursor != null) cursor.close() } return null } override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. return when (item.itemId) { R.id.action_settings -> true else -> super.onOptionsItemSelected(item) } } //下ごしらえ ②-2 パーミッションの関数の追加 // Permissionの確認 @TargetApi(Build.VERSION_CODES.M) fun checkPermission() { if ( ContextCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED) { Log.d("checkPermission", "PERMISSION GRANTED") // 既に許可している readContentActivity() } else { Log.d("checkPermission", "PERMISSION DENIDED") // 拒否していた場合 requestLocationPermission() } } //下ごしらえ ②-3 パーミッション-許可を求める @TargetApi(Build.VERSION_CODES.M) private fun requestLocationPermission() { if (shouldShowRequestPermissionRationale( Manifest.permission.READ_EXTERNAL_STORAGE) ) { requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_PERMISSION ) } else { Toast.makeText(this, "許可されないとアプリが実行できません", Toast.LENGTH_SHORT ).show() requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_PERMISSION) } } //下ごしらえ ②-4 パーミッションの結果の受け取り override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { if (requestCode == REQUEST_PERMISSION) { // 使用が許可された Log.d("onRequestPermissions()", "PERMISSION GRANTED") if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d("onRequestPermissions()", "To read content activity") readContentActivity() } else { Log.d("onRequestPermissions()", "CAN'T do anymore") // それでも拒否された時の対応 Toast.makeText(this,"これ以上なにもできません", Toast.LENGTH_SHORT).show() } } } // Intent でActivityへ移行 private fun readContentActivity() { // val intent = Intent(application, ReadContent::class.java) // startActivity(intent) } }