androidandroid-jetpack-composeandroid-contentresolverandroid-external-storage

Access Images In External Storage Created By My App


I need a method to access the images stored in external storage by my app.

I want to access those images without requesting READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES from the user

I'm using ACTION_GET_CONTENT to get the image from the user.

val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()){
        val uri : String = it.data?.data?.toString()?:"null"
        if (uri != "null"){
            val mimeType = context.contentResolver.getType(uri.toUri()).toString()
            it.data?.data?.let {
                returnUri ->
                context.contentResolver.query(returnUri, null, null, null, null)
            }?.use { cursor ->
                val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
                cursor.moveToFirst()
                val name = cursor.getString(nameIndex)
                val size = cursor.getLong(sizeIndex)
                image = image.copy(
                    path = uri, mimeType = mimeType.replace("image/",""), size = size,
                    name = name, uri = it.data?.data
                )
            }
        }else{
            image = image.copy(path = uri)
        }
    }

Calling the launcher for result

launcher.launch(Intent(Intent.ACTION_GET_CONTENT).setType("image/*"))

After performing the required actions on the image, I save the image using the following method.

fun saveFile(context : Context, file: File) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            try {
                val values = ContentValues()
                val path = Environment.DIRECTORY_PICTURES + "/FolderName"
                values.put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)
                values.put(MediaStore.MediaColumns.MIME_TYPE, "image/*")
                values.put(MediaStore.MediaColumns.RELATIVE_PATH, path)
                val savedFile = context.contentResolver.insert(Media.EXTERNAL_CONTENT_URI, values)
                val outputStream = savedFile?.let {
                    context.contentResolver.openOutputStream(it)
                }
                val fis = FileInputStream(file)
                var length : Int
                val buffer = ByteArray(8192)
                while (fis.read(buffer).also { length = it } > 0)
                    outputStream?.write(buffer, 0, length)
                Toast.makeText(context, "Picture saved to $path", Toast.LENGTH_SHORT).show()
                println("Picture : $path / $savedFile")
            }catch (e : IOException){
                Toast.makeText(
                    context,
                    "An error occured while saving the file",
                    Toast.LENGTH_SHORT
                ).show()
                e.printStackTrace()
            }catch (e : Exception) { e.printStackTrace() }
        }else{
            try {
                val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).absolutePath + "/FolderName"
                val image = File(dir, file.name)
                val fis = FileInputStream(file)
                val fos = FileOutputStream(image)
                var length : Int
                val buffer = ByteArray(8192)
                while (fis.read(buffer).also { length = it } > 0) {
                    fos.write(buffer, 0, length)
                }
            }catch (e : IOException){
                Toast.makeText(
                    context,
                    "An Error occurred while saving the file",
                    Toast.LENGTH_SHORT
                ).show()
                e.printStackTrace()
            }catch (e : Exception) { e.printStackTrace() }
        }
    }

psst ~ All of these actions are performed without requesting any permissions.

When I'm trying to access images from contentResolver, it always returns 0.

private fun loadImages() : List<Image> {

        val photos = mutableListOf<Image>()

        val collection = sdk29andUp {
            MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        } ?: MediaStore.Images.Media.EXTERNAL_CONTENT_URI

        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.DATE_ADDED
        )

        contentResolver.query(
            collection, projection, null, null, null
        )?.use { cursor ->

            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
            val displayNameColumn =
                cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
            val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)

            cursor.moveToFirst()
            while (cursor.moveToNext()) {
                val id = cursor.getLong(idColumn)
                val name = cursor.getString(displayNameColumn)
                val date = cursor.getLong(dateAddedColumn)

                val contentUri = ContentUris.withAppendedId(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    id
                )

                photos.add(
                    Image(
                        id = id, name = name, entryDate = date, contentUri = contentUri
                    ))

            }


        }
        return photos.toList()
    }

If someone can help me with this, I would really appreciate that.

Edit : The app crashes on API 28 and Before, so I'll have to request permissions for those API levels, but it means there is a solution for api's after 28


Solution

  • I was able to fix it with a very straight forward implementation. Posting the code in hopes that it will help someone.

    This implementation covers all API levels from 21 to 33

    Saving the image

    /** import MediaStore.Images.Media to remove Redundant typing of MediaStore.Images.Media **/
    private fun saveImage(file: File) {
            sdk29andUp {
                val collection = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                val path = Environment.DIRECTORY_PICTURES + "/" + getString(R.string.app_name)
                val values = ContentValues().apply {
                    put(Media.DISPLAY_NAME, file.name)
                    put(Media.SIZE, file.length())
                    put(Media.RELATIVE_PATH, path)
                }
    
                contentResolver.insert(collection, values)?.also { uri ->
                    contentResolver.openOutputStream(uri)?.use { outputStream ->
                        val fis = FileInputStream(file)
                        var length : Int
                        val buffer =  ByteArray(8192)
                        while (fis.read(buffer).also { length = it } > 0)
                            outputStream.write(buffer, 0, length)
                    }
                    println(uri)
                } ?: throw IOException("Error creating entry in mediaStore")
            } ?: saveImageBefore29(file)
        }
    
        private fun saveImageBefore29(file: File) {
            val resolver = contentResolver
            val collection = Media.EXTERNAL_CONTENT_URI
            val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).absolutePath + "/${getString(R.string.app_name)}"
            val directory = File(dir)
            if (!directory.exists())
                directory.mkdirs()
            val image = File(dir, file.name).also { println(it) }
            val values = ContentValues().apply {
                put(Media.DISPLAY_NAME, image.name)
                put(Media.SIZE, image.length())
                put(Media.MIME_TYPE, "image/png")
                put(Media.DATA, image.path)
            }
            resolver.insert(collection, values)?.also { uri ->
                contentResolver.openOutputStream(uri)?.use { outputStream ->
                    val fis = FileInputStream(file)
                    var length : Int
                    val buffer = ByteArray(8192)
                    while (fis.read(buffer).also { length = it } > 0)
                        outputStream.write(buffer, 0, length)
                }
            }
    
        }
    

    sdk29andUp function ->

    inline fun <T> sdk29andUp(onSdk29: () -> T) : T? =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
            onSdk29() 
        else null
    

    Some things I want to point out are:

    API 29 and above: Creating the folder you want to save the file in before this function would result in IllegalArgumentException as your app doesn't own that folder. Just insert the folder path in RELATIVE_PATH and resolver will create the folder in which your app can write freely.

    API 28 and below: You will need to create the folder yourself in these API levels while also requesting the READ_EXTERNAL_STORAGE & WRITE_EXTERNAL_STORAGE from the user.

    Loading The Images

    private fun loadImages() : List<Image> {
            return sdk29andUp {
    
                val imagesList = mutableListOf<Image>()
                val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
                val projection = arrayOf(
                    MediaStore.Images.Media._ID,
                    MediaStore.Images.Media.DISPLAY_NAME,
                    MediaStore.Images.Media.DATE_MODIFIED
                )
    
                contentResolver.query(
                    collection,
                    projection,
                    null, null,
                    null
                )?.use { cursor ->
                    val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
                    val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
                    val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED)
    
                    while (cursor.moveToNext()){
                        val id = cursor.getLong(idColumn)
                        val name = cursor.getString(nameColumn)
                        val date = cursor.getLong(dateColumn)
    
                        val contentUri = ContentUris.withAppendedId(collection, id).also { println("URI $it") }
                        imagesList += Image(
                            id = id, name = name, path = contentUri.path.toString(),
                            contentUri = contentUri, entryDate = date
                        )
                    }
                    imagesList
                }
    
            } ?: loadImagesBefore29()
        }
    
        private fun loadImagesBefore29() : List<Image> {
            val images = mutableListOf<Image>()
            val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            val projection = arrayOf(
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.DATE_TAKEN,
                MediaStore.Images.Media.DATA
            )
    
            val selection = "${MediaStore.Images.Media.DATA} like ? "
            val selectionArgs = arrayOf("%${getString(R.string.app_name)}%")
    
    
            contentResolver.query(
                collection,
                projection,
                selection,
                selectionArgs,
                null
            )?.use { cursor ->
                val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
                val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
                val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN)
                val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
    
                while (cursor.moveToNext()){
                    val id = cursor.getLong(idColumn)
                    val name = cursor.getString(nameColumn)
                    val date = cursor.getLong(dateColumn)
                    val data = cursor.getString(dataColumn)
    
                    val contentUri = ContentUris.withAppendedId(
                        collection, id
                    ).also { println("Uri $it") }
    
                    images += Image(
                        id = id, name = name, path = data,
                        contentUri = contentUri, entryDate = date
                    )
    
                }
    
            }
            return images
        }
    

    When loading the images on API level 29 and above, if your app doesn't have READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permission granted, ContentResolver will only query the MediaStore for images/files your app created.

    On API level 28 and below, I'm querying a specific folder as the app holds READ_EXTERNAL_STORAGE so contentResolver will return all images in MediaStore if no selectionArgs are specified.

    Hope I was able to explain this and help someone with this.