Search code examples
androidandroid-permissionsandroid-fileandroid-11scoped-storage

Storage Access Framework- Permission denied issue even when permissions granted


I am implementing Storage Access Framework to grant Android folder access. I have updated targetSDKVersion=30 and on Android 11 i am having storage permissions issues. I want to get storage permission to access files inside Android/media/com.whatsapp/WhatsApp/Media.

To make storage permissions work i have implemented Storage Access Framework to get Android/media/com.whatsapp/WhatsApp/Media access and then fetch files inside its subfolders (i.e .Statuses, WhatsAppImages)

Below are code details

     <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="com.android.vending.BILLING" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.ACTION_MANAGE_OVERLAY_PERMISSION" />

 <application
        android:name=".application.MyApplication"
        android:allowBackup="false"
        android:hardwareAccelerated="true"
        android:label="@string/app_name"
        android:largeHeap="true"
        android:requestLegacyExternalStorage="true" // also tried removing this
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:replace="android:allowBackup">

Activity Code

   //First i checked and requested android.permission.READ_EXTERNAL_STORAGE and android.permission.WRITE_EXTERNAL_STORAGE

if ((ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    )
            == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.READ_EXTERNAL_STORAGE
    )
            == PackageManager.PERMISSION_GRANTED)
) {
    //further work of opening directory
} else {

    TedPermission.with(this)
        .setPermissionListener(sdk30PermissionListener)
        .setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
        .setPermissions(
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE
        )
        .check()
}

//This is the root path i want to hit to access all subfolders inside this one

public static final String whatsApp_root_path = "Android/media/com.whatsapp/WhatsApp/Media”;
companion object {
   const val ANDROID_DOCID = "primary:${Constants.whatsApp_root_path}"
    const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
        "com.android.externalstorage.documents"
    private val androidUri = DocumentsContract.buildDocumentUri(
        EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
    )
    val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
        EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
    )
}

private val handleIntentActivityResult =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode != Activity.RESULT_OK)
            return@registerForActivityResult
        val directoryUri = it.data?.data ?: return@registerForActivityResult
        contentResolver.takePersistableUriPermission(
            directoryUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        if (checkIfGotAccess()) {
            premiumDialogHandling()
            onGotAccess()
        } else {
            Log.d("AppLog", "you didn't grant permission to the correct folder")
            tinyDB.putBoolean("SDK30Permissions", false)
            showWrongFolderSelection()
        }
    }
private fun checkIfGotAccess(): Boolean {
    return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
        uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
    } >= 0
}
private fun openDirectory() {
    if (checkIfGotAccess()) {
        onGotAccess()
    } else {
        val primaryStorageVolume =
            (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
        val intent = primaryStorageVolume.createOpenDocumentTreeIntent()
            .putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidUri)
        handleIntentActivityResult.launch(intent)

    }
}


private fun onGotAccess() {
    tinyDB.putBoolean("SDK30Permissions", true)
//once user chooses ‘Use Folder’ to allow permissions
  processStatusFetchExecute()


}
public fun processStatusFetchExecute(completed: () -> Unit) {
    Timber.e("processStatusFetch:Init")

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        @Suppress("DEPRECATION")
       var statusesFolder = File(
            Environment.getExternalStorageDirectory(),
            Constants.whatsApp_root_path
        ).listFiles(FileFilter { file -> file.name.equals("${Constants.FOLDER_NAME_STATUSES}") })

        val docIdStatuses = "${HomeActivity.ANDROID_DOCID}/${statusesFolder.get(0).name}"
        val childrenUriStatuses =
            DocumentsContract.buildChildDocumentsUriUsingTree(
                HomeActivity.androidTreeUri,
                docIdStatuses
            )

        val statusTreeUri = DocumentsContract.buildTreeDocumentUri(
            HomeActivity.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, docIdStatuses
        )

        val hasAccess: Boolean =
            contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
                uriPermission.uri.equals( HomeActivity.androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
            } >= 0
        
        Timber.e("processStatusFetch:Execute")
        val statusImages = arrayListOf<File>()
        val statusVideos = arrayListOf<File>()

        val png = MimeTypeMap.getSingleton().getMimeTypeFromExtension("png")
        val jpg = MimeTypeMap.getSingleton().getMimeTypeFromExtension("jpg")
        val jpeg = MimeTypeMap.getSingleton().getMimeTypeFromExtension("jpeg")

        val args = arrayOf(png, jpg, jpeg)
        val where = (MediaStore.Files.FileColumns.MIME_TYPE + "=?"
                + " OR " + MediaStore.Files.FileColumns.MIME_TYPE + "=?"
                + " OR " + MediaStore.Files.FileColumns.MIME_TYPE + "=?")
        var fileCursorExternal: Cursor? = null
        val orderBy = MediaStore.Files.FileColumns.DATE_MODIFIED
        val column = arrayOf(
            MediaStore.Files.FileColumns.DISPLAY_NAME,
            MediaStore.Files.FileColumns.MIME_TYPE,
            MediaStore.Files.FileColumns.DATE_MODIFIED
        )
        //Please note args,where, orderby are not working with contentResolver.query so i have handled file filter using loop

        val selectionMimeType = MediaStore.Files.FileColumns.MIME_TYPE + "=?"
        val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension("jpg")
        val selectionArgsPdf = arrayOf(mimeType)
        fileCursorExternal = contentResolver.query(
            childrenUriStatuses!!,
            column,
            selectionMimeType,
            selectionArgsPdf,
            "date_modified DESC"
        )
        GlobalScope.launch(Dispatchers.Main + exceptionHandler) {

            async(Dispatchers.IO + exceptionHandler) {
                Timber.e("processStatusFetch:InProgress")
                while (fileCursorExternal!!.moveToNext()) {
                    val nameIndex =
                        fileCursorExternal.getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME)

                    val displayName = fileCursorExternal.getString(nameIndex)
                    val path =
                        "${Constants.whatsApp_root_path}/${Constants.FOLDER_NAME_STATUSES}/$displayName"
                    @Suppress("DEPRECATION")

                    if (getFileType(displayName) == FILETYPE.IMAGE) {
                        statusImages.add(
                            File(
                                Environment.getExternalStorageDirectory(),
                                path
                            )
                        )
                    } else if (getFileType(displayName) == FILETYPE.VIDEO) {
                        statusVideos.add(
                            File(
                                Environment.getExternalStorageDirectory(),
                                path
                            )
                        )
                    }
                }
            }.await()
            Timber.e("processStatusFetch:Done")
            sharedViewModel.statusImages.postValue(statusImages)
            sharedViewModel.statusVideos.postValue(statusVideos)
            completed()
        }


    }
}

What's done so far:

Using above code i have successfully displayed Media screen for use folder. enter image description here

And i am also able to successfully fetch all the files inside .Statuses folder

Problem:

When i set list of images to Recyclerview inside RecyclerViewAdapter. On setting file to Imageview i always getting Permissions Denied exception.

Method 1: Tried setting file to image view using Glide

Glide.with(itemView.context).load(file.absoluteFile)
    .listener(object :
        RequestListener<Drawable> {
        override fun onLoadFailed(
            e: GlideException?,
            model: Any?,
            target: Target<Drawable>?,
            isFirstResource: Boolean
        ): Boolean {
                        return false
        }

        override fun onResourceReady(
            resource: Drawable?,
            model: Any?,
            target: Target<Drawable>?,
            dataSource: DataSource?,
            isFirstResource: Boolean
        ): Boolean {
            val abc = "yes"

            return false
        }
    }
    ).into(imgview)

Got below exception

com.bumptech.glide.load.engine.GlideException: Failed to load resource
There were 3 root causes:
java.io.FileNotFoundException(/storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg: open failed: EACCES (Permission denied))
java.io.FileNotFoundException(/storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg: open failed: EACCES (Permission denied))
java.io.FileNotFoundException(open failed: EACCES (Permission denied))
 call GlideException#logRootCauses(String) for more detail

Method 2: Tried setting file to image view using Uri

imgview.setImageURI(Uri.fromFile(file))

Got below exception

resolveUri failed on bad bitmap uri: file:///storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/25340b4ddcf44eb2a8d6a5d0509feee9.jpg
W/ImageView: Unable to open content: file:///storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg
    java.io.FileNotFoundException: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg: open failed: EACCES (Permission denied)
        at libcore.io.IoBridge.open(IoBridge.java:492)
        at java.io.FileInputStream.<init>(FileInputStream.java:160)
        at java.io.FileInputStream.<init>(FileInputStream.java:115)
        at android.content.ContentResolver.openInputStream(ContentResolver.java:1498)
        at android.graphics.ImageDecoder$ContentResolverSource.createImageDecoder(ImageDecoder.java:286)
        at android.graphics.ImageDecoder.decodeDrawableImpl(ImageDecoder.java:1758)
        at android.graphics.ImageDecoder.decodeDrawable(ImageDecoder.java:1751)
        at android.widget.ImageView.getDrawableFromUri(ImageView.java:1011)
        at android.widget.ImageView.resolveUri(ImageView.java:980)
        at android.widget.ImageView.setImageURI(ImageView.java:557)
        at androidx.appcompat.widget.AppCompatImageView.setImageURI(AppCompatImageView.java:120)
        at mypkgname.StatusImageAdapterNew$ImageViewHolder.bindItems(StatusImageAdapterNew.kt:135)
        at mypkgname.StatusImageAdapterNew.onBindViewHolder(StatusImageAdapterNew.kt:81)
        at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7065)
        at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7107)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6012)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6279)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:561)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
        at androidx.recyclerview.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:170)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at androidx.swiperefreshlayout.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:625)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1873)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at androidx.viewpager.widget.ViewPager.onLayout(ViewPager.java:1775)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1873)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at androidx.drawerlayout.widget.DrawerLayout.onLayout(DrawerLayout.java:1263)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
        at com.android.internal.policy.DecorView.onLayout(DecorView.java:797)
        at android.view.View.layout(View.java:23242)
        at android.view.ViewGroup.layout(ViewGroup.java:6513)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3694)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3152)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2123)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8601)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1035)
        at android.view.Choreographer.doCallbacks(Choreographer.java:858)
        at android.view.Choreographer.doFrame(Choreographer.java:789)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1020)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:236)
        at android.app.ActivityThread.main(ActivityThread.java:8051)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:620)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1011)
     Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
        at libcore.io.Linux.open(Native Method)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254)
        at libcore.io.ForwardingOs.open(ForwardingOs.java:166)
        at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7923)
        at libcore.io.IoBridge.open(IoBridge.java:478)
        ... 85 more
    resolveUri failed on bad bitmap uri: file:///storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg

Reading both exceptions i can understand that the permissions i granted for Media folder inside Android may not be correct but it returns true when i check it. Please note this is happening only on Android 11 because i updated SDK version to 30.

I am unable to resolve this issue i think i need to get bit more knowledge of Storage Access Framework to set these permissions correct because after that i need to work on writing/copying files.


Solution

  • Reading both exceptions i can understand that the permissions i granted for Media folder inside Android may not be correct

    You cannot grant anything. Instead you were granted access to Whatsapp Media folder. But only while using SAF.

    Now you have a bunch of irrelevant code where you define providers and paths. No good. Scary code.

    And while you may succeed in listing all the files you are not using the uries of all those files (You should put them in an <Uri> list to be used by the recycle view) but with again all kind of tricks build up a File path.

    So throw away all those provider and path definitions.

    Just list the content of the Media folder with a direct query(). If you see the ".Statuses" folder listed then go and list that folder. And.. save the obtained file uries to an Uri list.

    You started with SAF and converted to File. Dont do such nasty things. Your SAF permissions are no File permissions. You do not need any permission in manifest to use SAF.