I'm building a built-in file manager for my app and I'm using MediaStore to query for the files.
I got everything working, except that the URI I get back is not to Glide's liking.
class java.io.FileNotFoundException: /external/images/media/37: open failed: ENOENT (No such file or directory)
I understand the issue at hand is that there's no protocol added so glide doesn't know how to read it, but I don't know how to add the protocol, I was under the impression that using ContentUris.withAppendedId would handle that for me but it appears to not be.
val mediaList = mutableListOf<Uri>()
val query = buildQuery(folderId, onlyImages)
query?.use { cursor ->
val columnIndexId = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val columnIndexMimeType = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
val columnIndexDuration = cursor.getColumnIndexOrThrow(MediaStore.Video.VideoColumns.DURATION)
while (cursor.moveToNext())
{
val id = cursor.getLong(columnIndexId)
val contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
mediaList.add(contentUri)
}
}
return mediaList
I'm trying to void using DATA as I already know that's depreciated and I just need this solution work with Android 8+.
Are you converting the contentUri to a path? You shouldn't be doing that. Your contentUri should look like it does below, and then Glide will accept it:
content://media/external/images/media/37
Related
As part of my app, users can select images from their device and I store the URLs to these in my database. Then, elsewhere in the app, I need to display those images.
I am getting the image URI through Intent.ACTION_PICK:
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
imagePickerAction = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
if (uri != null) {
val uriString = uri.toString()
saveToDatabase(uriString)
}
}
This results in a URI string like content://com.android.providers.media.documents/document/image%3A1000000038.
I am able to show the image using ImageView.setImageURI at this point.
But later on, when I fetch the URI from the database and try to use setImageURI again, I get this error:
java.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{cdd4e32 10499:com.skamz.shadercam/u0a160} (pid=10499, uid=10160) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
I have also tried following the docs here https://developer.android.com/training/data-storage/shared/documents-files#open:
private fun getBitmapFromUri(uri: Uri): Bitmap {
val parcelFileDescriptor: ParcelFileDescriptor? =
contentResolver.openFileDescriptor(uri, "r")
if (parcelFileDescriptor == null) {
return bitmapFromUri(contentResolver, Uri.parse(TextureOverlayShaderData.defaultImageUrl))
}
val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
parcelFileDescriptor.close()
return image
}
This returns a bitmap, which I then pass into ImageView.bitmapFromUri. Unfortunately, this produces the same error, this time coming from contentResolver.openFileDescriptor(uri, "r")
when I fetch the URI from the database
That's not an option with ACTION_PICK. You cannot reliably persist a Uri that you received from ACTION_PICK and use it again later. Your access rights to the content are short-lived.
If you switch to ACTION_OPEN_DOCUMENT and use takePersistableUriPermissions() on a ContentResolver when you get the Uri in onActivityResult(), you can safely persist that Uri. You might still run into problems using it later — for example, the user could delete the image that the Uri points to — but you will not encounter the access problem that you are seeing here.
Having invoked a directory selector on Android with:
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
)
activity?.startActivityForResult(intent, REQUEST_CODE_FOLDER_PERMISSION)
And having obtained the URI of said route in onActivityResult(), being the URI of the form (example in case of having chosen a folder named backup in the root of the external storage):
content://com.android.externalstorage.documents/tree/primary:backup
At this point, how do you write a file to that location? After researching various answers on how to write files using the Media Store API, all the examples I've seen use constants to refer to already existing media directories, but in my case I want to create a new document (which is a JSON file) in the directory chosen by the user.
You will not use the MediaStore to save a file if you obtained an uri using ACTION_OPEN_DOCUMENT_TREE to get permission for a folder.
Just continue to use Storage Access Framework and implement DocumentFile.createFile().
Pretty basic exercise for learning SAF.
If you want to use the MediaStore to save a file then you do not have the user to select a folder first.
Thanks to #CommonsWare for pointing me in the right direction:
var outputStream: OutputStream? = null
try {
val uri = Uri.parse(path)
val document = DocumentFile.fromTreeUri(context, uri)
val file = document?.createFile(mimeType, filename)
?: throw Exception("Created file is null, cannot continue")
val fileUri = file.uri
val contentResolver = context.contentResolver
outputStream = contentResolver.openOutputStream(fileUri)
val bytes = content.toByteArray()
outputStream?.write(bytes)
outputStream?.flush()
} catch (e: Exception) {
// Handle error
} finally {
outputStream?.close()
}
I'm building an App that manages audiobook libraries
Using the Intent ACTION_OPEN_DOCUMENT_TREE to let the user choose a Directory as a library, I get an Uri of the form : content:// as a result.
Is there a way to convert the given "content://" Uri to a "file:// filepath" ? ( if that is possible of course )
Or can I tweak the file chooser activity to accept only folders that have an actual file:// path ?
Thank you very much
EDIT : progress !
I managed, using the content resolver, to find a path of the form "1407-1105:Audiobooks" for the SD card, and "primary:Audiobooks" for the main volume. That seems more readable, but I have the same problem still.
Finally found the solution ! Maybe it is a little bit ugly, but that does seems to work !
fun resolveContentUri(uri:Uri): String {
val docUri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
val docCursor = context.contentResolver.query(docUri, null, null, null, null)
var str:String = ""
// get a string of the form : primary:Audiobooks or 1407-1105:Audiobooks
while(docCursor!!.moveToNext()) {
str = docCursor.getString(0)
if(str.matches(Regex(".*:.*"))) break //Maybe useless
}
docCursor.close()
val split = str.split(":")
val base: File =
if(split[0] == "primary") getExternalStorageDirectory()
else File("/storage/${split[0]}")
if(!base.isDirectory) throw Exception("'$uri' cannot be resolved in a valid path")
return File(base,split[1]).canonicalPath
}
I added files to Documents/MyExcelsFolder by using ContentResolver.insert and then also added new file to Documents/MyExcelsFolder folder by another app (for ex. FileManager)
Then I try to get all files from the MyExcelsFolder folder
fun getAppFiles(context: Context): List<AppFile> {
val appFiles = mutableListOf<AppFile>()
val contentResolver = context.contentResolver
val columns = mutableListOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
add(
MediaStore.MediaColumns.RELATIVE_PATH
)
}
}.toTypedArray()
val extensions = listOf("xls", "xlsx")
val mimes = extensions.map { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) }
val selection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
"${MediaStore.MediaColumns.RELATIVE_PATH} LIKE ?"
} else {
"${MediaStore.Images.Media.DATA} LIKE ?"
}
val selectionArgs = arrayOf(
"%${Environment.DIRECTORY_DOCUMENTS}/MyExcelsFolder%"
)
contentResolver.query(
MediaStore.Files.getContentUri("external"),
columns,
selection,
selectionArgs,
MediaStore.Files.FileColumns.DATE_ADDED + " DESC"
)?.use { cursor ->
while (cursor.moveToNext()) {
val pathColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val mimeColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)
val filePath = cursor.getString(pathColumnIndex)
val mimeType = cursor.getString(mimeColumnIndex)
if (mimeType != null && mimes.contains(mimeType)) {
// handle cursor
appFiles.add(cursor.toAppFile())
} else {
// need to check extension, because the Mime Type is null
val extension = File(filePath).extension
if (extensions.contains(extension)) {
// handle cursor
appFiles.add(cursor.toAppFile())
}
}
}
}
return appFiles
}
fun Cursor.toAppFile(): AppFile {
val cursor = this
val idColumnIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)
val nameColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val mimeColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)
val pathColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val id = cursor.getLong(idColumnIndex)
val uri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id)
val fileDisplayName = cursor.getString(nameColumnIndex)
val filePath = cursor.getString(pathColumnIndex)
var mimeType = cursor.getString(mimeColumnIndex)
val relativePath = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH))
} else {
null
}
var type = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
if (type == null) {
type = File(filePath).extension
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(type)
}
return AppFile(
id = id,
uri = uri,
absolutePath = filePath,
name = fileDisplayName,
mimeType = mimeType,
extension = type,
relativePath = relativePath
)
}
And in result there are only files from ContentResolver added by insert command, and there no files copied by FileManager. How to see all files in cursor?
Operation system: Android 10 (Q) (API level 29)
Target API version: api 29
Starting from Android 10 there is a new storage access model in action which is called Scoped Storage and it is much more restrictive. In short:
Your app can always access its own directories.
Your app can write (with help of ContentResolver.insert) to the shared media collections and can read only files created by your app from them. You can access other apps files from these collections by requesting the READ_EXTERNAL_STORAGE permission.
Your app can access other files and directories by using file or directory system pickers.
It's a bit weird and looks like a bug that you are able to access xls files through the MediaStore.Files collection. The documentation says
The media store also includes a collection called MediaStore.Files.
Its contents depend on whether your app uses scoped storage, available
on apps that target Android 10 or higher:
If scoped storage is enabled, the collection shows only the photos,
videos, and audio files that your app has created.
If scoped storage
is unavailable or not being used, the collection shows all types of
media files.
But anyway you still are not able to access files created by other apps as stated above.
So depending on your use case there are a few options to go:
As accessing files through the MediaStore.Files works for you now, you can try to request READ_EXTERNAL_STORAGE permission as shown in this table to get a non-filtered access to media collections. But I would expect such way to work unreliably on different devices and/or expect to stop it working with the new updates, because the media collections are supposed to be used for media files only.
You can use ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE to show a file/directory picker to user and get access to a file or the whole directory tree. Please check the restrictions of this method also. I'd say this is the preferable way to go.
Android 10 allows you to temporarily opt-out from the scoped storage by using the android:requestLegacyExternalStorage flag. But Android 11 is out already and this flag does not have any effect in it.
You can request the new MANAGE_EXTERNAL_STORAGE permission and then request a special white-listing by user to access all files. That's how the file managers work now. This feature is available starting from Android 11, so you'll likely to use the opt-out flag for Android 10. Also be sure to check the restrictions and Google Play policies on using this feature if you are going to publish your app there.
New to Kotlin and i'm trying to simple get any video from gallery and open into device's default app player.
My function to get all videos. It seens to work well, but the returned Uri is like 'content://...', i don't know if this is the right or it should be something like 'file://...'
private val videos = mutableListOf<Uri>()
private fun getAllVideos() {
val uriExternal = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Video.Media._ID)
contentResolver.query(uriExternal, projection, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val videoUri = Uri.withAppendedPath(uriExternal, "" + cursor.getString(0))
videos.add(videoUri)
}
}
}
Then i try to open the Uri like this, but i always get an error from the player and nothing works.
val intent = Intent(Intent.ACTION_VIEW).apply {
data = videos.first()
type = "video/*"
}
startActivity(intent)
I searched but dont found any updated tutorial that don't use "MediaStore.Video.Media.DATA" (it's deprecated now). Something i'm doing wrong?
but the returned Uri is like 'content://...', i don't know if this is the right
Yes, it is.
Then i try to open the Uri like this, but i always get an error from the player and nothing works.
First, either remove the type or use the correct MIME type. Do not use a wildcard.
Second, add FLAG_GRANT_READ_URI_PERMISSION to the Intent. Without it, the other app has no rights to access the content.
Also, make sure you only go through that code if there is at least one element in the list, as otherwise your first() call will throw an exception.