Sharing cached images using FileProvider - android

I have an app that uses Universal Image Loader to download photos from the internet, cache them to data/data/com.myapp/cache and display in ImageViews.
I also wanted to add sharing (WhatsApp, Facebook, Instagram, Dropbox etc.) to my app, so I tried to use this code:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, photoUri);
intent.setType("image/jpeg");
startActivity(Intent.createChooser(intent, "Share image"));
photoUri was the uri in cache folder which other apps don't have permission to read.
So I googled/stackoverflowed and found out that I need to use FileProvider.
I configured the FileProvider as follows (as was written here) in my manifest:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.myapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths" />
</provider>
And the file_paths.xml:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="photo_cache" path="/"/>
</paths>
Then I use the following code in my activity in button onClickListener:
File photoFile = ImageLoader.getInstance().getDiscCache().get("HTTP LINK HERE");
// returns File of "/data/data/com.myapp/cache/-1301123243"
Uri photoUri = FileProvider.getUriForFile(MyActivity.this, "com.myapp.fileprovider", photoFile);
// returns Uri of "content://com.myapp.fileprovider/photo_cache/-1301123243"
photoUri = Uri.parse(photoUri.toString() + ".jpg");
// then I add .jpg to file name
// Create intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, photoUri);
intent.setType("image/jpeg");
// Grant permissions to all apps that can handle this intent
// thanks to this answer http://stackoverflow.com/a/18332000
List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
// And start
startActivity(Intent.createChooser(intent, "Share image"));
However, depending on the app I'm getting strange exceptions. Like this:
1974-1974/com.whatsapp W/Bundle﹕ Key android.intent.extra.STREAM expected ArrayList but value was a android.net.Uri$HierarchicalUri. The default value <null> was returned.
1974-1974/com.whatsapp W/Bundle﹕ Attempt to cast generated internal exception:
java.lang.ClassCastException: android.net.Uri$HierarchicalUri cannot be cast to java.util.ArrayList
at android.os.Bundle.getParcelableArrayList(Bundle.java:1223)
at android.content.Intent.getParcelableArrayListExtra(Intent.java:4425)
at com.whatsapp.ContactPicker.d(ContactPicker.java:320)
at com.whatsapp.ContactPicker.onCreate(ContactPicker.java:306)
at android.app.Activity.performCreate(Activity.java:5104)
Or I just see logs like this:
591-963/system_process W/ActivityManager﹕ Permission denied: checkComponentPermission()
And of course apps themselves give me toasts with errors (that they can't open or download the file).
I tried, googled, stackoverflowed A LOT, so now I'm posting here.
I guess I'm doing it right, maybe just some little thing is wrong...
Any help will be appreciated!

I think this might be an an issue with either the FileProvider infrastructure or the application you're sharing with.
I have been trying to use this this support to share images w/ other applications. It works fine w/ Gallery, Drive, Gmail and Keep. It fails with Photos and Google+.
I see entries like the following in LogCat which leads me to believe that the Uri is being passed from one Activity to another. However, the (temporary) permission set in the Intent is being lost.
11-19 21:14:06.031: I/ActivityManager(433): Displayed com.google.android.apps.plus/.phone.HostPhotoViewIntentActivity: +848ms

Related

createChooser() intent exception "requires the provider be exported, or grantUriPermission()"

The app needs to share a PDF file stored in the root of the cacheDir with other apps. The issue is seen on Android 12, possibly other versions too.
Manifest:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths" />
</provider>
Provider paths:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="." />
</paths>
Intent:
val pdfFile = File(requireContext().cacheDir, pdfFileName)
val fileUri: Uri = FileProvider.getUriForFile(
requireContext().applicationContext,
requireContext().packageName.toString() + ".provider",
pdfFile
)
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.type = "application/pdf"
startActivity(Intent.createChooser(intent, "Share Document"))
The share sheet successfully opens but this exception always shows at that point and subsequently sharing to another app fails.
Writing exception to parcel
java.lang.SecurityException: Permission Denial: reading
androidx.core.content.FileProvider uri
content://uk.co.packagename.provider/cache/8BEDF7212-0DE46-42B0-9FA9-32C434BDD2F3HO.pdf
from pid=15363, uid=1000 requires the provider be exported, or grantUriPermission()
The provider as a whole cannot be exported and the URI permission appears to already be granted. I've read through the Android file sharing docs and many S/O answers but I cannot see what needs correcting, can you?
One of the limitations of FileProvider.getUriForFile() is that it does not check to see if the file exists. There are legit reasons for getting a Uri to a file that does not exist, such as for ACTION_IMAGE_CAPTURE. Still, it means that just getting the Uri is no guarantee that that the Uri is useful for reading content.
Compounding that problem is that "does the file exist" via exists() feels like it may be a bit dicey, especially for external storage.
So, it's pretty important to make sure that you have the right File object, and that it should point to an already-existing file, before you call getUriForFile().
After checking what package caused this exception it turned out to be android system. So after granting uri permission, I see no more exceptions like this in the logs:
requireContext().grantUriPermission("android", fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
The reason a security exception ("requires the provider be exported, or grantUriPermission()") is thrown is because the android system is trying to access the shared resource to give the user a preview of what would be shared.
The best approach is to use the intent.setClipData to share the resource in addition to intent.putExtra(Intent.EXTRA_STREAM, fileUri)
The code should look like this:
intent.action = Intent.ACTION_SEND
intent.setClipData(ClipData.newRawUri("", fileUri))
intent.putExtra(Intent.EXTRA_STREAM, fileUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
#skvalex's answer above works, but it is a hacky way of giving the Android system access to the file for preview in the application share picker screen.
For a additional reference on this, please take a look at the FileProvider class as well.

How do you share images via an intent in an instant app?

We are building a quiz instant app, where the user can complete the quiz and then share their result. We share some text with a link, and also an image that shows the user's quiz result. There is no problem when we go through this flow in the installed app, however in the instant app, the image fails to share.
Here is how we generate the intent:
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", image)
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, content)
putExtra(Intent.EXTRA_STREAM, uri)
type = "image/*"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
activity?.startActivity(Intent.createChooser(shareIntent, getString(R.string.quiz_share_title)))
Here is the provider in our base application manifest:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/fileprovider" />
</provider>
When the user shares the image in the instant app, this error message appears in logcat:
java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.redacted.fileprovider/shared/1563809004297.png from pid=29184, uid=1000 requires the provider be exported, or grantUriPermission()
I have tried setting exported="true", and that crashes the instant app on startup with the following exception:
java.lang.RuntimeException: Unable to get provider androidx.core.content.FileProvider: java.lang.SecurityException: Provider must not be exported
I'm guessing that instant apps can't use the FLAG_GRANT_READ_URI_PERMISSION flag, for the same reason that they can't use the WRITE_EXTERNAL_STORAGE permission.
Is there another way we can share images in instant apps?
An instant app can not have an exported ContentProvider. This is a security restriction and crashing the app here is working as intended.
You could use InstantApps.showInstallPrompt() before firing the Intent in order to get users to install the app before doing this. Please make sure that you display a message containing your rationale or otherwise you might confuse your users.
There are other ways to share images using instant apps. But these depend on where the image is coming from. In case of an external content provider (i.e. the Camera app's) you should be able to forward the URI.
use this piece of code to share image from directory:
private void shareImage() {
Intent share = new Intent(Intent.ACTION_SEND);
// If you want to share a png image only, you can do:
// setType("image/png"); OR for jpeg: setType("image/jpeg");
share.setType("image/*");
// Make sure you put example png image named myImage.png in your
// directory
String imagePath = Environment.getExternalStorageDirectory()
+ "/myImage.png";
File imageFileToShare = new File(imagePath);
Uri uri = Uri.fromFile(imageFileToShare);
share.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(share, "Share Image!"));
}
You can share anythings using Intent class
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, "Image URL");
startActivity(intent);

File Sharing in Android

I am new to development with android, i am recently working with an android application that exports document into PDF (conversion) tool , the problem is after exporting of PDF i want to give a option to user to share the document(PDF) through intent, i have digged around stackoverflow but was not able to understand and answers were not actually answering my question. The PDF is exported/created into external sd card.
I have created a PDF through my application after exporting/creating the PDF i want to share them through intent , i have digged around stackoverflow but dint get answer.how can i share it through intent, like i share with image,text through intent.
Answer of user #oleonardomachado is correct but from android N update direct uri share is prohibited. you have to use file provider to get uri data and then share.
Share using intent
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
if (Build.VERSION.SDK_INT >= 24) {
Uri fileUri = FileProvider.getUriForFile(getContext(), getPackageName()+".fileprovider", file); // provider app name
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
shareIntent.setType("application/pdf");
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
shareIntent.setType("application/pdf");
}
startActivity(Intent.createChooser(shareIntent, "Share PDF"));
In AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
...
<application
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.my.package.name.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths"/>
</provider>
</application>
</manifest>
And after that create a file_paths.xml file in res/xml folder. xml Folder may be not there so create if it doesn't exist. The content of the file is shown below. It describes that we would like to share access to the External Storage at root folder (path=".").
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>
Hope this will help others.
You can use ACTION_SEND to activate the chooser for the specified file type, just remember to provide "application/pdf" as the file type.
public void SharePdf(File file) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
shareIntent.setType("application/pdf");
startActivity(Intent.createChooser(shareIntent, "Share PDF"));
}
Now call SharePdf(new File(fileName)) to start the intent and let the user select the correct option.

Share Privately Stored Image to External App - Android

I'm trying to save an image to my app's private filesystem then share that image with external apps. I've looked into it, and it seems like the best way to do it is just to save onto the filesDir because if you use the externalFiles directory, some devices may not have one. Here's my code so far (simplified for brevity):
Activity with my image
public Uri saveImageAndGetUri(Context context) {
String fileName = "test.png";
File imageDirectory = new File(getFilesDir(), "images");
File savedImage = new File(imageDirectory, fileName);
FileOutputStream stream;
try {
stream = new FileOutputStream(savedImage);
getCurrentImage().compress(Bitmap.CompressFormat.PNG, 100, stream);
stream.flush();
stream.close();
} catch(Exception e) {}
return FileProvider.getUriForFile(context, "com.mydomain.mypackage.Dialogs.ShareOptions", savedImage);
}
ShareOptions.java
Uri contentUri = activity.saveImageAndGetUri(getContext()); //This calls the method above
intent = new Intent(Intent.ACTION_SEND);
getContext().grantUriPermission("com.twitter.android", contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); //Grant read permission to Twitter. Don't think this part is working
getContext().grantUriPermission("com.twitter.android", contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); //Grant write permission to Twitter. Don't think this part is working
intent.setPackage("com.twitter.android");
intent.setType("image/png");
intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_STREAM, contentUri);
AndroidManifest.xml
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.mypackage.Dialogs.ShareOptions"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/file_paths"/>
</provider>
xml/file_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
</paths>
As soon as the intent is sent, the Twitter app crashes. I've looked into the error log in Twitter and it seems like it may be an issue with permissions. I think somehow Twitter doesn't have permission to access the Uri even with the code I currently have.
Any and all help is appreciated!
The reason it doesn't work is many 3rd-party apps don't support FileProvider right now. See this question: Image share intent works for Gmail but crashes FB and twitter.

android - share image via intent - uri permissions

Referring to my previous question.
I want to ask something specific to per-URI permissions. I understand that to send binary data like an image whose URI is held by a provider in your app, two things must be done :
1) Provider should set android:grantUriPermission
2) Uri permissions must be granted either via Intent.setFlags or context.grantUriPermission
I wanted to see the effect when I don't do step 2. My provider is :
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.android.provider.DataSharing"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/paths"/>
</provider>
App1 sharing the image with App2 does the following :
File imagePath = new File(getApplicationContext().getFilesDir(), "images");
File newFile = new File(imagePath, "earth.jpg");
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(),
"com.android.provider.DataSharing", newFile);
Intent i = new Intent();
i.setAction(Intent.ACTION_SEND);
i.putExtra(Intent.EXTRA_STREAM, contentUri);
i.setType("image/jpeg");
startActivity(i);
My receiving app (App2) does :
Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (imageUri != null){
ImageView iv = (ImageView) findViewById(R.id.imageView);
iv.setImageURI(imageUri);
}
But somehow even without granting the URI permission through intent or context, I can see the image in App2's image view. I am testing in Android 4.4.
Can someone help me explain this behavior ? What did I do that App2 still has the rights to access the image ?
According to the documentation for FileProvider, Context.grantUriPermission():
The permission remains in effect until you revoke it by calling
revokeUriPermission() or until the device reboots.
However, if you use the Intent flag:
Permissions granted in an Intent remain in effect while the stack of
the receiving Activity is active. When the stack finishes, the
permissions are automatically removed.
So it looks like it's the expected behavior (if you're testing with the same image / same Uri).

Categories

Resources