[Kotlin] RecyclerView その 1.6 – 画像取得とURI/パス、パーミッションについて

本文上広告1



経緯

アルバムアプリのようなものを作ろうとしています。今回調査した機能としては、その中の「画像を選択してパスを取得、パスをデータベースに保存。そのデータベースからパスの一覧等を取得、画像を表示する」機能の部分。RecylcerViewに画像を表示させることを考えています。

SAF(Storage Access Framework)で画像を取り出すのは簡単そうでしたし、実際にやってみると簡単でした。

今回の到達点 エミュレータに画像を保存。ボタン(FAB *)でIntentをセットしてギャラリーを起動(SAF *)。ギャラリ...

そこでSAFのURIがMedia Storeを使用する方法でも扱えると安易に考えて、軽いサンプルコードであたりをつけはじめました。(そして、しばらくはまりました。)

画像を取得するピッカをIntentで起動して(SAF にて)、URIを取得して画像化する部分に入力すればOKかとおもっていましたが、画像化する部分でエラーが発生。簡単にはちょちょいと解決できなさそうでしたので、ちょっと本腰を入れて調査することにしました。

調査結果 その1.画像取得に関して

画像を取得するときには、現在2種類方法があります。

  1. SAF(Storage Access Framework)を使用する方法
  2. ContentProvider(Media Store)を使用する方法

SAF(Storage Access Framework)を使用する方法の参考URL

ContentProvider(Media Store)を使用する方法の参考URL

調査結果 その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取得→変換→パス取得」を行います。コードを追加していく元のサンプルとしては、以下を参照ください。

今回の到達点 エミュレータに画像を保存。ボタン(FAB *)でIntentをセットしてギャラリーを起動(SAF *)。ギャラリ...

全体のコードは長いので、最後に載せています。

  1. 下ごしらえ①として、AndroidManifest.xml にusers-permissionの記述を入れます
  2. 
    
  3. 下ごしらえ②として、 onCreate()内にパーミッションの関数を追加。パーミッションの関数はこちらの”[Android] MediaStore スマホの画像や音楽ファイルを検索する” を参照
  4. 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();
        }
    
  5. その他下ごしらえとして、パーミッションの関数を追加しています。②-1~4を参照ください。
  6. SAFでURIを取得。
  7. 今回の到達点 エミュレータに画像を保存。ボタン(FAB *)でIntentをセットしてギャラリーを起動(SAF *)。ギャラリ...
  8. コードとしては、onActivityResult()内で、"uri = resultData.data"の部分
  9. stackoverflowのandroid - cursor didn't have _data column not foundの関数をKotlinに変換して使用。パスが取得できました。下ごしらえ③として、コードを見てくだください
  10. var path: String? = getPathFromUri( applicationContext , uri)
    
  11. 変換関数にてパスを取得したので、今回のサンプルではパスの方で画像を表示します。"[Android] 外部ストレージに画像を保存・読出しをする"の読み出し参照。
  12. val file = File(path)
    val inputStream0 = FileInputStream(file)
    val bitmap = BitmapFactory.decodeStream(inputStream0)
    imageView.setImageBitmap(bitmap)
    
  13. 変換したものもログに出して確認します。
  14. D/onActivityResult: content://com.android.providers.media.documents/document/image%3A54
    D/onActivityResult Path: /storage/emulated/0/DCIM/Camera/IMG_20190331_100436.jpg
    
  15. 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)
    }


}

シェアする

  • このエントリーをはてなブックマークに追加

フォローする