I'm struggling to understand what could be a simple workaround for associating images and persistent data objects in android. In more details, i've put up a simple room persistence architecture and now I need to add a field "image" to the java persisted object. I've tried to work with uri but my knowledge of Android is very poor, and what I get is that the uri I recover when picking an image with the android file manager is only valid until reboot, so if I would save the so obtained uri in the database, it would make no sense when recovered later. How should I manage?
Basically what I need is a simple way to link an object to a local image stored in the phone (or captured on the fly with the camera), no worries about image deletion by the user or anything, just a simple way.
For istance I tried to tinker with the google code example but i clearly failed because I don't know what i'm doing
private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
this code results in compilation error, with required: parcedDescriptor... and VOID found, on the call of takePersistableUriPermission. I don't even know if that is a solution to my problem.
this is the code I use to get the uri from the local image, but I'm planning also to let the Camera snap a photo and pass it to for saving/linking it
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "*/*".
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
the uri I recover when picking an image with the android file manager is only valid until reboot, so if I would save the so obtained uri in the database, it would make no sense when recovered later
That's not quite accurate.
A Uri that you pull in via ACTION_OPEN_DOCUMENT will be good for whatever activity gets the Uri via onActivityResult(). If you pass that Uri to another component, you can use FLAG_GRANT_READ_URI_PERMISSION to allow that component to read the content at that Uri. But once your process ends, your access to that content goes away.
Since you used ACTION_OPEN_DOCUMENT, you can use takePersistableUriPermission() request to have long-term access to the content, but that still only works if the content is still there. If the user deletes the content, or perhaps even moves it, you will lose access.
For istance I tried to tinker with the google code example but i clearly failed because I don't know what i'm doing
takePersistableUriPermission() does not return a ParcelFileDescriptor. Otherwise, that particular call seems OK.
With respect to loading the image, please use an existing image-loading library (e.g., Glide, Picasso).
Related
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.
I have an app in which I'd like to view a video file that I chose to share from the Gallery to be edited. For that I know I have to use the intent-filter tag in the manifest of the Activity.
So here I was, selecting a video from the Gallery app to be 'shared' into this app. When the Activity opens, I used Uri videoUri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); to retrieve the URI of the video. The URI looks like this:
content://com.google.android.apps.photos.contentprovider/-1/2/file%3A%2F%2F%2Fdata%2Fdata%2Fcom.google.android.apps.photos%2Fcache%2Fshare-cache%2Fmedia.tmp%3Ffilename%3Dimage.jpg/ORIGINAL/NONE/1804018524
I can play it just fine in MediaPlayer, but the problem comes when I have to use a library that requires a File [namely, MP4Parser], and that means a file path - this URI doesn't work.
I've parsed the file%3A%2F%2F%2Fdata%2Fdata%2Fcom.google.android.apps.photos%2Fcache%2Fshare-cache%2Fmedia.tmp%3Ffilename%3Dimage.jpg part, and it gives me:
file:///data/data/com.google.android.apps.photos/cache/share-cache/media.tmp?filename=image.jpg
The problem with this is that opening it results in an ENOENT. new File("/data/data/com.google.android.apps.photos/cache/share-cache/media.tmp").exists() returns false.
I've also queried the URI itself in the cursor via getContentResolver().query(uri, null, null, null, null) and logged all the values inside. All I got was:
_id:0
_display_name:image.jpg
_size:2183131
mime_type:video/mpeg
_data:null
orientation:0
datetaken:0
latitude:null
longitude:null
special_type_id:null
My question: Is there a way to inquire the actual path [either in file:// form or in content://media/external/videos/...] of the video shared by the Gallery app if that URI is what it sends out?
Turns out all I needed to do is to open an InputStream from said URI via the ContentResolver, write it into a FileOutputStream directed towards a temporary file (right now I have the temporary file inside my app's FilesDir), then use that as the data source instead.
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.
The final objective will be clear shortly.
I want to create a file object and instead of getting data from a real physical file I want to provide the buffer myself.
Then, I want to use this file, which does not really exist in the sdcard or anywhere outside my app, give it a name and send it by email as an attachment (using the EXTRA_STREAM).
I found the following bit of code, by Adriaan Koster (#adriaankoster), the post Write byte[] to File in Java
// convert byte[] to File
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
File fileFromBytes = (File) ois.readObject();
bis.close();
ois.close();
System.out.println(fileFromBytes);
I used it to create this function
private File fileFromBytes(byte[] buf) {
File f = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(buf);
ObjectInputStream ois = new ObjectInputStream(bis);
f = (File) ois.readObject();
bis.close();
ois.close();
}
catch (Exception e) {}
return f;
}
and here is where I am stuck, because when I use it:
// When sent as body the mail is sent OK
// emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, dump());
// When I try to attach the mail is empty
emailIntent.putExtra(android.content.Intent.EXTRA_STREAM, fileFromBytes(dump().getBytes()));
I know from examples I've seen the second argument should be an URI, but: How do I create a virtual URI to fit my file?
EDIT:
The option to attach data directly from within the application is important to certain kind of applications. Namely, security & banking applications that do not want to move sensitive data around too much. Surely if the data does not reach the sdcard and goes directly to a mail attachment it is harder to sniff than within the application memory.
This is not my specific case, but I wanted to point out that this capability is important to have.
The first thing you'll want to do, I imagine, is create a ContentProvider. You can see an example implementation here
https://github.com/dskinner/AndroidWeb/blob/master/src/org/tsg/web/WebContentProvider.java
where in the above link's case, you would add this to your AndroidManifest.xml
<provider
android:name="org.tsg.web.WebContentProvider"
android:authorities="your.package.name" />
Now, you'll have a content uri available for use, content://your.package.name/.
The portion of the above ContentProvider your interested in, again I imagine, is the openFile method. When sharing data by intent across apps, certain things are expected. In your case, you're looking to share some byte data that's meant to be attached to the email.
So if you pass in a content uri to the email app such as content://your.package.name/foo with the appropriate intent flags, then openFile will get called on your ContentProvider. In this case, you can inspect the end of the uri segment to see foo was requested, and return appropriately.
The next issue you bring up is not having the file actually on disk. While I can't vouch for the method you used above (though it looks kosher), what you need to be returning is a ParcelFileDescriptor from your ContentProvider. If you look at the link I provided, you could possibly try to use that as a sample to get the file descriptor from your File object (my knowledge waivers here), but I imagine, the data simply wont be available at that point.
What you do bring up is security though. It's important to note that you can write data to disk privately so only the app has access to the data. I believe, but you might want to double check on this, if that data is private to the app, you can expose it via the ContentProvider and possibly lock down who and how the provider gets used, who can call it, etc. You may want to dig into android docs for that portion or look at some other SO questions.
Anyway, good luck.
Create the file in the application's cache directory. It will be created in the internal filesystem. Use 'getCacheDir()' API for getting the path to the cache dir. Write the data into this dir and then get the URI from the File object using ' Uri.fromFile (File file) '. When you are finished with the file, delete it.
Your application's cache is only available to your app, hence its safe to use for your purpose.
You can do some encryption if the data is too critical.
I think in order to do this, you are going to have to expose a ContentProvider, which will allow you handle a URI. The email application should then openInputStream on your URI, at which point you return an InputStream on your in-memory data.
I've not tried it, but in theory this should work.
i was busy with adding attachment to mail and i can send mail with attachment.
if you want to take a look: can not send mail with attachment in 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!