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.
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.
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.