Handle response from ACTION_GET_CONTENT - android

Does anyone have a recommendation for the best way to handle URI results from the ACTION_GET_CONTENT intent?
I am finding that applications that handle the intent provide different data back in return. When selecting files from the Download directory in three different file pickers I get significantly different results.
The KitKat download gallery:
content://com.android.providers.downloads.documents/document/1438
Genymotion (CyanogenMod) Browser:
file:///storage/emulated/0/Download/334SIGCO-PHRH-FEB14.xls
Android
File Manager:
content://com.smartwho.SmartFileManager/mimetype//storage/emulated/0/Download/334
PhrPrint.xls
I'm concerned that when I release my app I will get responses back from the user that I didn't anticipate in code and it will crash. Given that the three tools I've tested have all provided different answers, I'm concerned for what will happen in the wild.
Ultimately I will need to read the contents of the selected file into an InputStream and read the data (.xls via jxl) into my data structure. But first I need to check the data to get the file name and verify the data type so I can provide feedback to the user that they have a valid file type.
I can use the ContentResolver to do that with the KitKat Download Gallery, but the code fails for both of the other options. Code follows:
public void setFileUri(Uri fileUri) {
ContentResolver resolver = mFragment.getActivity().getContentResolver();
//Check if the file is of the right data type
if(resolver.getType(fileUri).equals(REQUIRED_FILE_TYPE)) {
mFileUri = fileUri;
mComplete = true;
}
Cursor cursor = resolver.query(fileUri, null, null, null, null);
cursor.moveToFirst();
((FileSelectFragment)mFragment).setFileNameView(cursor.getString(
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)),
mComplete);
}
Thank you in advance.

Related

Android 10: How to delete MediaStore item and it's associated data on file system programmatically?

I am updating my app to use Scoped Storage feature introduced in Android 10.
My app works with MediaStore and displays images, videos and audio files and provides ability for user to delete item.
What I did earlier to delete file:
Got path from MediaStore.MediaColumns.DATA
Used new File(path).delete() to delete that file
Manually updating MediaStore
Now that MediaStore.MediaColumns.DATA is not available I migrated to deleting items from MediaStore using ContentResolver.delete()
For example I have uri of the item: content://media/external/images/media/502 (its valid uri, I display it's thumbnail in grid). It doesnt matter whether I inserted this item in MediaStore or some other app did.
I use context.getContentResolver().delete(uri, null, null). It either succeeds in deletion (returns 1 row) or catching RecoverableSecurityException to use startIntentSenderForResult() to get access to current uri and then using the same getContentResolver().delete() to delete it in onActivityResult() and then getting successful deletion.
Either way that item is removed from MediaStore and is neither showing in result when I query MediaStore for images, nor in other applications.
BUT this file exists on file system (checked using SAF and various file managers (Google Files, Total Commander))
Sometimes (depends on Android version and media type) these items are brought back to MediaStore after phone reboot (or after opening Google Photos - it scans file system)
For example: Android 10 on my Google Pixel and Google Pixel 3a XL behaves as described above for Images/Video/Audio, but Android 9 on Xiaomi Mi A2 Lite behaves like this only with Audio files, while deleting Images/Video fine.
I have android:requestLegacyExternalStorage="false" in manifest.
Is there a way to force MediaStore to delete data on file system as well? Why is file on file system left behind?
Yes, as you have pointed out that's how we had to delete media files. We have to delete the physical copy of the file by forming a File object and also delete the indexed file in MediaStore using ContentResolver.delete() (or) do a media scan on the deleted file which would remove it's entry in MediaStore.
This is how it used to work in below Android 10 os. And would still work the same in Android 10 as well if you had opted out of scoped storage by specifying it in manifest android:requestLegacyExternalStorage="true"
Now in Android 11 you are forced to use scoped storage. If you want to delete any media file which is not created by you, you have to get the permission from the user. You can get the permission using MediaStore.createDeleteRequest(). This will show a dialog by description what operation users are about to perform, once the permission is granted, android has an internal code to take care of deleting both the physical file and the entry in MediaStore.
private void requestDeletePermission(List<Uri> uriList){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
PendingIntent pi = MediaStore.createDeleteRequest(mActivity.getContentResolver(), uriList);
try {
startIntentSenderForResult(pi.getIntentSender(), REQUEST_PERM_DELETE, null, 0, 0,
0);
} catch (SendIntentException e) { }
}
}
The above code would do both, requesting the permission to delete, and once permission granted delete the files as well.
And the callback result you would get it in onActivityResult()
Use this function to delete file using display name of the file:
This func will delete MediaStore item and it's associated data on file system in Android-10 or Android-Q
Note: In my case I am working with files like MediaStore.Files.FileColumns..
public static boolean deleteFileUsingDisplayName(Context context, String displayName) {
Uri uri = getUriFromDisplayName(context, displayName);
if (uri != null) {
final ContentResolver resolver = context.getContentResolver();
String[] selectionArgsPdf = new String[]{displayName};
try {
resolver.delete(uri, MediaStore.Files.FileColumns.DISPLAY_NAME + "=?", selectionArgsPdf);
return true;
} catch (Exception ex) {
ex.printStackTrace();
// show some alert message
}
}
return false;
}
Use this function to get Uri from DisplayName
public static Uri getUriFromDisplayName(Context context, String displayName) {
String[] projection;
projection = new String[]{MediaStore.Files.FileColumns._ID};
// TODO This will break if we have no matching item in the MediaStore.
Cursor cursor = context.getContentResolver().query(extUri, projection,
MediaStore.Files.FileColumns.DISPLAY_NAME + " LIKE ?", new String[]{displayName}, null);
assert cursor != null;
cursor.moveToFirst();
if (cursor.getCount() > 0) {
int columnIndex = cursor.getColumnIndex(projection[0]);
long fileId = cursor.getLong(columnIndex);
cursor.close();
return Uri.parse(extUri.toString() + "/" + fileId);
} else {
return null;
}
}
According to my observations there is no force delete.
I usually add several checks if a file has really been deleted
on Android Q it is also not possible to delete an entire album without the user manually confirming each file. this makes deleting files on the device uninteresting for me
I'm also experiencing the same issue, but with Huawei models where it fails with image & video files.
I've found that there are some reported bugs about it, although Google have discarded 2 of them because they can't reproduce it.
I would advice that you star them and add some more details.
https://issuetracker.google.com/issues/157714528
https://issuetracker.google.com/issues/142270549
https://issuetracker.google.com/issues/145348304
Update:
I've created a new issue ticket, as some of the above have been discarded by google as not reproducible:
https://issuetracker.google.com/issues/184355104
I fixed this problem in my app for Android 10 by opting out of scoped storage (android:requestLegacyExternalStorage="true") and requesting WRITE_EXTERNAL_STORAGE for Android 10 as well:
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
Then ContentResolver.delete will not trigger RecoverableSecurityException and will also delete the file from disk. Given you requested and was granted Manifest.permission.WRITE_EXTERNAL_STORAGE permission.
Note: If you don't care about deleting files not created by your app or created by previous installations of your app, then you don't need to request that permission.

Media scanner for secondary storage on Android Q

With the newer Android Q many things changed, especially with scoped storage and gradual deprecation of file:/// URIs. The problem is the lack of documentation on how to handle media files correctly on Android Q devices.
I have a media file (audio) management application and I could not find yet a reliable way to tell to the OS that I performed a change to a file so that it can update its MediaStore record.
Option #1: MediaScannerService
MediaScannerConnection.scanFile(context, new String[]{ filePath }, new String[]{"audio/*"}, new MediaScannerConnection.OnScanCompletedListener() {
#Override
public void onScanCompleted(String s, Uri uri) {
}
});
Works with file:// URIs from primary storage
Not works with file:// URIs from secondary storage (such as removable storage)
Not works with any content:// URI
Option #2: broadcast
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
Not working at all
Soon deprecated
Option #3: manual MediaStore insertion
AudioFileContentValues are some column values from MediaStore.Audio.AudioColumns.
Old method based on file:// URI:
Uri uri = MediaStore.Audio.Media.getContentUriForPath(file_path);
newUri = context.getContentResolver().insert(uri, AudioFileContentValues);
MediaStore.Audio.Media.getContentUriForPath is deprecated
Still not working
Newer method based on what I could put together from documentation:
Uri collection = MediaStore.Audio.Media.getContentUri(correctVolume);
newUri = context.getContentResolver().insert(collection, AudioFileContentValues);
Where correctVolume would be external from primary storage, while it would be something like 0000-0000 for secondary storage, depending on where the file is located.
Insertion returns a content URI such as content://media/external/audio/media/125 but then no record is persisted inside MediaStore for files located in primary storage
Insertion fails with no URI returned and no record in MediaStore
These are more or less all the methods available in previous Android versions but none of them now allow me to notify the system that I changed some audio file metadata and to get Android to update MediaStore records. Event though option #1 is partially working, this could never be a valuable solution because it's clearly not supporting content URIs.
Is there any reliable way to trigger media scan on Android Q, despite where the file is located? We shouldn't even care about file location, according to Google, since we will soon only use content URIs. MediaStore has always been a little frustrating in my opinion, but now the situation is pretty worse.
I'm also currently struggling with that.
I think what you want to do you cannot do any longer once you are on Android Q, because you are not allowed to access the Music directory on Q. You are only allowed to create and access files in directories you created. You did not create the music directory.
Now every change to the Media has to happen threw the MediaStore. So you insert your Music file beforehand and then get an outputstream from the MediaStore to write to it. All the changes on Q on Media should be done threw the MediaStore hence you informing the MediaStore of changes cannot even occur anymore, because you never directly access the File.
This has one giant caviat in that all the new things in MediaStore that make that possible do not exist in older versions of Android. So I do currently believe that you will need to implement everything twice, sadly. At least if you want to actively influences where your music is saved to that is.
Those two MediaStore columns are new in Q and do not exist before Q, witch you'll probably need to use in Q
MediaStore.Audio.Media.RELATIVE_PATH with that you can influence the path where it's saved. So I put "Music/MyAppName/MyLibraryName" there and that will end up saving "song.mp3" into "Music/MyAppName/MyLibraryName/song.mp3"
MediaStore.Audio.Media.IS_PENDING this you should be setting to 1 while the song is still being written and then afterwards you can update it to 0.
I've also now started to implement things twice with if checks for Android versions. It's annoying. I don't want to do it. But it seems like that's the only way.
I'm just gonna put a bit of code here on how I managed inserting music on Android.Q and below. It's not perfect. I have to specify the MIME type for Q, because flacs would now become .flac.mp3 somehow, because it does not quite seem to get that.
So, anyways this is a part that I have updated already to work with Q and before, it downloads a Music file from a music player on my NAS. The app is written in kotlin, not sure if that's a problem for you.
override fun execute(library : Library, remoteApi: RemoteApi, ctx: Context) : Boolean {
var success = false
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = ContentValues().apply {
put(MediaStore.Audio.Media.RELATIVE_PATH, library.rootFolderRelativePath)
put(MediaStore.Audio.Media.DISPLAY_NAME, remoteLibraryEntry.getFilename())
put(MediaStore.Audio.Media.IS_PENDING, 1)
}
val collection = MediaStore.Audio.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val uri = ctx.contentResolver.insert(collection, values)
ctx.contentResolver.openOutputStream(uri!!).use {
success = remoteApi.downloadMusic(remoteLibraryEntry, it!!)
}
if(success) {
values.clear()
val songId = JDrop.mediaHelper.getSongId(uri)
JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
values.put(MediaStore.Audio.Media.IS_PENDING, 0)
ctx.contentResolver.update(uri, values, null, null)
} else {
ctx.contentResolver.delete(uri, null, null)
}
} else {
val file = File("${library.rootFolderPublicDirectory}/${remoteLibraryEntry.getFilename()}")
if(file.exists()) file.delete()
success = remoteApi.downloadMusic(remoteLibraryEntry, file.outputStream())
if (success) {
MediaScannerConnection.scanFile(ctx, arrayOf(file.path), arrayOf("audio/*")) { _, uri ->
val songId = JDrop.mediaHelper.getSongId(uri)
JDrop.db.music.insert(Music(mediaStoreId = songId, remoteId = remoteLibraryEntry.remoteId, libraryId = library.id))
}
}
}
return success
}
And the MediaStoreHelper Method being this here
fun getSongId(uri : Uri) : Long {
val cursor = resolver.query(uri, arrayOf(Media._ID), null, null, null)
return if(cursor != null && cursor.moveToNext()) {
val idIndex = cursor.getColumnIndex(Media._ID)
val id = cursor.getLong(idIndex)
cursor.close()
id
} else {
cursor?.close()
-1
}
}
One thing when you do not specify the MIME type it seems to assume mp3 is the MIME type. So .flac files would get saved as name.flac.mp3, because it adds the mp3 file type if there is none and it thinks it's a mp3. It does not add another .mp3 for mp3 files. I don't currently have the MIME type anywhere... so I'm gonna go ahead and do this now, I guess.
There is also a helpful google IO talk about scoped/shared storage https://youtu.be/3EtBw5s9iRY
That probably won't answer all of your questions. It sure enough didn't for me. But it was a helpful start to have a rough idea what they even did change to begin with.
For deleting and updating files its kinda the same on Q if you call delete on a mediastore entry, the file will be deleted. Before, Q you have to manually delete the file also. But if you do that on Q your app will crash. So again you have to check wether or not youre on Q or an older version of android and take appropriate actions.

How to force a MediaStore update for a file using only its Content Uri

In Android Q the field MediaStore.Files.FileColumns.DATA has been deprecated, and may be Null or apps have no rights to read it when targeting such OS version, so will be preferable to work using only a file’s content Uri.
Since the MediaScannerConnection only accepts file paths, I found that for Android Q this is no longer an option.
What would be the way to force an automatic MediaStore update/re-scan of a single file, without knowing its real path and using only its Uri? The intention is not to even try to find the real path and rely only in the Uri.
Consider that the Uri to force the update is a media content Uri (not a SAF Uri).
Example: content://media/external/images/media/123
The solution must not be to re-scan the entire storage or un-mount / mount the storage again, as this will have a high performance hit in our workflow and will make it completely unusable.
And because the intention is to use only the Uri, then to avoid forcing a scan of any specific directory of files, which would also have an impact if it contains lots of files, and implies that a real directory path must be resolved from the Uri, which is not an option.
UPDATE:
We have tried with unsuccessful results the ContentResolver.refresh method introduced in Android O, yet this method doesn't do any refresh at all when it comes to a media content Uri in a format such as: content://media/external/images/media/123
final ContentResolver resolver = context.getContentResolver();
resolver.refresh(uri, null, null);
I'm currently also trying to redesign my app to use just URIs rather than using all of the hacky solutions to convert them to filepaths (like guessing the path based on the uri authority, etc.) Previously, I used MediaScannerConnection.scanFile(...), which worked without a flaw, but as you've probably already tried, this doesn't work with URIs.
I am finding success by manually updating the MediaStore URI with my new data like this:
public void updateMediaStore(final Uri content, final String title) {
final ContentValues values = new ContentValues();
values.put(MediaStore.Audio.AudioColumns.TITLE, title);
// ... the rest of your data
cr.update(res, values, null, null);
}
Still, it seems like an oversight to not provide a way to rescan a specific file. For example, if this URI comes from somewhere else, such as on an sdcard via SAF, you will have to first search for it in the MediaStore before updating it.

_data column not available at FileProvider uri

I'm using the FileProvider pattern for creating content:// uri to files, with the
FileProvider.getUriForFile(this, "com.myapp.provider", file)
function. I have the manifest, provider_paths and everything set the standard way, It creates an uri like content://com.myapp.provider/external_files/music/mysong.mp3.
My issue is that if I try getting the real file path in another app, it doesn't work as the _data column doesn't exist (to be specific the error in logs is E/CursorWindow: Failed to read row 0, column -1 from a CursorWindow which has 1 rows, 0 columns.). For fetching the real path I'm using the also pretty much standard function
final String column = MediaStore.Files.FileColumns.DATA;
final String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
If I use a different app for sharing the same file it generates an uri like content://com.otherapp.provider/external_files/music/mysong.mp3, from which I can already retrieve the real file path. Any ideas what do I have to do to make sure that my app properly inserts the given uri to ContentResolver? Manual contentResolver.insert(...) functions are not allowed. I've tried different versions of provider_paths.xml and granting all possible read/write permissions to the given uri, but I could never retrieve the real path.
The uri itself generated by me works fine as I can read the file or play the song, my issue is just that I cannot retrieve the real file path that I need.
Thanks
My issue is that if I try getting the real file path in another app
The other app should not be trying to do this.
For fetching the real path I'm using the also pretty much standard function
That works for very few Uri values.
If I use a different app for sharing the same file it generates an uri like content://com.otherapp.provider/external_files/music/mysong.mp3, from which I can already retrieve the real file path.
That is not guaranteed.
Any ideas what do I have to do to make sure that my app properly inserts the given uri to ContentResolver?
You don't. You fix the client app, which should not be attempting to get a "real file path" from a Uri.
my issue is just that I cannot retrieve the real file path that I need.
Instead, for a Uri with a content scheme:
Step #1: Get a ContentResolver, by calling getContentResolver() on some Context (e.g., an activity)
Step #2: Call openInputStream() on the ContentResolver, passing in your Uri, to get an InputStream on that content
Step #3: Consume the content via that InputStream
If you are using some third-party library that can only work with files, copy the data from that InputStream to some FileOutputStream, then use the resulting file with that library.
This way, no matter where the content is coming from (a file that you could access, a file that you cannot access, a BLOB column in a database, etc.), you will have code that works.
See also:
Getting the Absolute File Path from Content URI for searched images
onActivityResult's intent.getPath() doesn't give me the correct filename
Android - Get real path of a .txt file selected from the file explorer

Sharing an Image in app's data directory through a ContentProvider on Android

I'm trying to expose a .png file located in my application's /data directory through a ContentProvider but instead of reaching the openFile method query is being called. Now I only ever have a single image which I need to expose for sharing to other applications, how can I setup my Intent to goto openFile instead of query?
Intent shareImageIntent = new Intent(Intent.ACTION_SEND);
shareImageIntent.setType("image/*");
shareImageIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
startActivity(Intent.createChooser(shareImageIntent, "Share image"));
Where the Uri looks like
content://my.package.contentprovider/fileName
Or alternatively do I need to create a database for this and return a cursor?
UPDATE
So this appears to be working on everything except the SMS app (which is what I decided to test first) I would like to support sharing to it however.
Here's the relevant stack trace:
Caused by: java.lang.IllegalArgumentException: Query on
content://mypackage.myprovider/someImage.png returns null result. at
com.android.mms.ui.UriImage.initFromContentUri(UriImage.java:104) at
com.android.mms.ui.UriImage.(UriImage.java:63) at
com.android.mms.model.ImageModel.initModelFromUri(ImageModel.java:83)
at com.android.mms.model.ImageModel.(ImageModel.java:65) at
com.android.mms.data.WorkingMessage.changeMedia(WorkingMessage.java:481)
at
com.android.mms.data.WorkingMessage.setAttachment(WorkingMessage.java:375)
...
So the SMS app is performing a query instead of reading directly from openFile, which every other app on my phone seems to do (including other Google apps)
Does anyone know what I need to return here to fullfil the query appropriately? I'm going to go AOSP digging now.
After digging through the source code of the SMS (MMS really) app this is what I came up with.
Inside UriImage.initFromContentUri the application makes the query code and assumes there are 2 returned columns in the Cursor
} else {
filePath = c.getString(c.getColumnIndexOrThrow(Images.Media.DATA));
mContentType = c.getString(c.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
}
So inorder for your ContentProvider to work with the MMS app, you need to return a Cursor in query that only has one row and the two columns (Images.Media.DATA & Images.Media.MIME_TYPE) with the appropriate data. The MMS will then make the call to openFile to actually retrieve the image.
An easier way to share a image resource is to save it to external storage (SD-card) and then do:
Uri imageUri = Uri.fromFile(pathToFile);
Update:
Try using
Uri imageUri = Uri.parse("android.resource://com.package.yourapp/" +imageResID);
Update2
Try saving file to Media Store and then sending it:
String url = Media.insertImage(context.getContentResolver(), imageFile.getAbsolutePath(), imageFile.getName(), imageFile.getName());
Uri imageUri = Uri.parse(url);
Final Update using ContentProvider and Cursor:
Your ContentProvider must implement query(..) method and it must return a Cursor. See the source code of UrlImage.initFromContentUri(..) (which is internally used by MMS app) to see how cursor is called. Take a look at the MatrixCursor if it fits the bill.
If your content provider is already working you can access to a ParcelFileDescriptor via the method openFileDescriptor in the content provider.
A quick, and dirty, example for this:
ParcelFileDescriptor descriptor = mContext.getContentResolver().openFileDescriptor(IMGURI, "r");
Bitmap bmp = BitmapFactory.decodeFileDescriptor(descriptor.getFileDescriptor());
Cheers!

Categories

Resources