存储访问框架 - 权限被拒绝问题,即使授予权限

Posted

技术标签:

【中文标题】存储访问框架 - 权限被拒绝问题,即使授予权限【英文标题】:Storage Access Framework- Permission denied issue even when permissions granted 【发布时间】:2021-12-18 06:01:21 【问题描述】:

我正在实施存储访问框架以授予 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">

活动代码

   //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 异常。

方法一:尝试使用 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 内的 Media 文件夹授予的权限可能不正确,但当我检查它时它返回 true。 请注意,这仅在 Android 11 上发生,因为我将 SDK 版本更新为 30。

我无法解决这个问题,我认为我需要更多地了解存储访问框架才能正确设置这些权限,因为之后我需要编写/复制文件。

【问题讨论】:

java.io.FileNotFoundException: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/.Statuses/6a8cb5d8dc8b4aba832c984c4f1e06c4.jpg: open failed: EACCES (Permission denied) 如果你使用 SAF,你永远不会有这个例外。 【参考方案1】:

阅读这两个例外我可以理解我为 Android 内的 Media 文件夹授予的权限可能不正确

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

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

虽然您可能会成功列出所有文件,但您并没有使用所有这些文件的 uris(您应该将它们放在 &lt;Uri&gt; 列表中以供回收视图使用),但再次使用各种技巧构建增加一个文件路径。

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

只需使用直接 query() 列出 Media 文件夹的内容。如果您看到列出的“.Statuses”文件夹,请列出该文件夹。并且..将获得的文件uris保存到一个Uri列表中。

您从 SAF 开始并转换为 File。不要做这么恶心的事。您的 SAF 权限不是文件权限。您不需要清单中的任何权限即可使用 SAF。

【讨论】:

知道了。在这篇博文之后,我以某种方式正确地实现了 SAF。 medium.com/swlh/… 正如你提到的,获得了一个 Uris 列表,可以使用这些 uri 在图像视图上显示,但问题是我的整个工作都是使用 File 对象,我无法改变整个事情。我的意思是我不能在需要将此 URI 转换为文件的任何地方使用 Uris。 我尝试使用 val fileObj=documentFile.uri.toFile 但它引发异常原因:java.lang.IllegalArgumentException:Uri 缺少“文件”方案:content://com.android.externalstorage。文档/树/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia/document/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses%2F0916765d27ae46b883ca7de728547c2f.jpg 异常很明显,因为它的 SAF uri 无法获取文件方案。有什么方法可以将此 SAF URI 转换为 java.io.File,以便我的条目功能可以像以前一样使用文件对象。这包括将文件下载到其他文件夹、共享文件、删除文件、在播放器上播放视频文件等。 或者有什么方法可以将 DocumentFile 转换为普通文件?如何继续前进?

以上是关于存储访问框架 - 权限被拒绝问题,即使授予权限的主要内容,如果未能解决你的问题,请参考以下文章

即使授予存储权限后,在三星手机上创建目录也被拒绝

即使用户授予位置权限检查,三星 note 20 也总是会被拒绝

即使在 Ar.js 中的浏览器中授予权限后,地理定位访问被拒绝弹出窗口也会显示

Android kotlin:即使在授予权限后也获得 EACCES(权限被拒绝)

AWS S3 访问被拒绝。存储桶权限被授予,那么当我是用户时,我需要存储桶策略吗?

即使授予正确的权限,Spring Security @Secured 也会拒绝访问