存储访问框架 - 权限被拒绝的问题,即使在授予权限时也是如此

Storage Access Framework- Permission denied issue even when permissions granted

我正在实施存储访问框架以授予 Android 文件夹访问权限。我已更新 targetSDKVersion=30 并在 Android 11 我遇到了存储权限问题。我想获得存储权限以访问 Android/media/com.whatsapp/WhatsApp/Media.

中的文件

为了使存储权限正常工作,我实施了存储访问框架以获得 Android/media/com.whatsapp/WhatsApp/Media 访问权限,然后在其子文件夹中获取文件 (即 .Statuses、WhatsAppImages)

下面是代码详情

     <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代码

   //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()
        }


    }
}

目前完成的内容:

使用上面的代码我已经成功显示了使用文件夹的媒体屏幕。

而且我也能够成功获取 .Statuses 文件夹中的所有文件

问题:

当我将图像列表设置为 RecyclerViewAdapter 内的 Recyclerview 时。在将文件设置为 Imageview 时,我总是收到 Permissions Denied 异常。

方法1:尝试使用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)

低于异常

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

方法 2:尝试使用 Uri 将文件设置为图像视图

imgview.setImageURI(Uri.fromFile(file))

低于异常

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

阅读这两个异常,我可以理解我在 Android 中授予媒体文件夹的权限可能不正确,但当我检查它时它 returns 是正确的。 请注意,这仅在 Android 11 上发生,因为我将 SDK 版本更新为 30。

我无法解决这个问题,我想我需要更多地了解存储访问框架才能正确设置这些权限,因为之后我需要处理 writing/copying 个文件。

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

你不能授予任何东西。相反,您被授予访问 Whatsapp Media 文件夹的权限。但仅在使用 SAF 时。

现在您有一堆不相关的代码,您可以在其中定义提供程序和路径。不好。可怕的代码。

虽然您可能成功列出了所有文件,但您并没有使用所有这些文件的 uri(您应该将它们放在 <Uri> 列表中以供回收视图使用)但是再次所有一种技巧建立文件路径。

所以扔掉所有那些提供程序和路径定义。

只需直接query()列出Media文件夹的内容即可。如果您看到列出的“.Statuses”文件夹,则去列出该文件夹。并且..将获取的文件uri保存到Uri列表中。

您从 SAF 开始并转换为文件。不要做这种讨厌的事情。您的 SAF 权限不是文件权限。您不需要清单中的任何权限即可使用 SAF。