存储访问框架 - 权限被拒绝问题,即使授予权限
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(您应该将它们放在 <Uri>
列表中以供回收视图使用),但再次使用各种技巧构建增加一个文件路径。
所以扔掉所有那些提供者和路径定义。
只需使用直接 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(权限被拒绝)