Reputation: 399
I am building a Flutter app that essentially fetches data from the cloud. The data type varies, but they're commonly an image, pdf, text file, or an archive (zip file).
Now I want to fire an implicit intent, so the user can choose their favorite app to handle the payload.
I've searched for answers, and I have tried the following routes:
Route #3 is not really what I wanted, since it's using the platform's "share" mechanism (ie. posting on Twitter / send to contact), instead of opening the payload.
Route 1 & 2 sort of worked... in a shaky, weird way. I'll explain later.
Here's the flow of my code:
import 'package:url_launcher/url_launcher.dart';
// ...
// retrieve payload from internet and save it to an External Storage location
File payload = await getPayload();
String uriToShare = samplePayload.uri.toString();
// at this point uriToShare looks like: 'file:///storage/emulated/0/jpg_example.jpg'
uriToShare = uriToShare.replaceFirst("file://", "content://");
// launch url
if (await canLaunch(uriToShare)) {
await launch(uriToShare);
} else {
throw "Failed to launch $uriToShare";
the above code was using url_launcher
plugin. If I was using android_intent
plugin, then the last lines of code becomes:
// fire intent
AndroidIntent intent = AndroidIntent(
action: "action_view",
data: uriToShare,
);
await intent.launch();
Everything up to saving the file to external directory works (I can confirm that the files exist after running the code)
Things get weird when I try to share the URI. I have tested this piece of code on 3 different phones. One of them (Samsung Galaxy S9) would throw this exception:
I/io.flutter.plugins.androidintent.AndroidIntentPlugin(10312): Sending intent Intent { act=android.intent.action.VIEW dat=content:///storage/emulated/0/jpg_example.jpg }
E/MethodChannel#plugins.flutter.io/android_intent(10312): Failed to handle method call
E/MethodChannel#plugins.flutter.io/android_intent(10312): java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.VIEW dat=content:///storage/emulated/0/jpg_example.jpg cmp=com.google.android.gm/.browse.TrampolineActivity } from ProcessRecord{6da6f74 10312:com.safe.fmeexpress/u0a218} (pid=10312, uid=10218) requires com.google.android.gm.permission.READ_GMAIL
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.os.Parcel.readException(Parcel.java:1959)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.os.Parcel.readException(Parcel.java:1905)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:4886)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.Instrumentation.execStartActivity(Instrumentation.java:1617)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.Activity.startActivityForResult(Activity.java:4564)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.Activity.startActivityForResult(Activity.java:4522)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.Activity.startActivity(Activity.java:4883)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.Activity.startActivity(Activity.java:4851)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at io.flutter.plugins.androidintent.AndroidIntentPlugin.onMethodCall(AndroidIntentPlugin.java:141)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:191)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at io.flutter.view.FlutterNativeView.handlePlatformMessage(FlutterNativeView.java:152)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.os.MessageQueue.nativePollOnce(Native Method)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.os.MessageQueue.next(MessageQueue.java:325)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.os.Looper.loop(Looper.java:142)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at android.app.ActivityThread.main(ActivityThread.java:6938)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at java.lang.reflect.Method.invoke(Native Method)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
E/MethodChannel#plugins.flutter.io/android_intent(10312): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)
I have no idea how the intent got polluted by cmp=com.google.android.gm/.browse.TrampolineActivity
This exception ONLY happens in Galaxy S9. Two other phones did not give me this problem. They did launch the file uri, and I was asked how to open the file, but none of the image-handling apps were being offered (ie, like Gallery, QuickPic, or Google Photos).
Just to clarify, both url_launcher
and android_intent
routes lead to the exact same results.
It feels like I'm missing a step here. Can anyone point out what I'm doing wrong? Do I have to start using platform channels to accomplish this?
Some clarifications on why I did what I did:
android.os.FileUriExposedException
android_intent
doesn't have a way to set intent flags just yet)Upvotes: 18
Views: 14550
Reputation: 486
Since this was answered, an excellent flutter plugin open_filex have been published, solving this cross platform.
When coupled with path_provider - in the following example downloading into getTemporaryDirectory()
, this opens a given url/filename with the associated app on iOS and Android (using the local file if already downloaded):
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:open_filex/open_filex.dart';
Future<String> download(String url, String filename) async {
String dir = (await getTemporaryDirectory()).path;
File file = File('$dir/$filename');
if (await file.exists()) return file.path;
await file.create(recursive: true);
var response = await http.get(url).timeout(Duration(seconds: 60));
if (response.statusCode == 200) {
await file.writeAsBytes(response.bodyBytes);
return file.path;
}
throw 'Download ${url} failed';
}
void downloadAndLaunch(String url, String filename) {
download(url, filename).then((String path) {
OpenFilex.open(path);
});
}
-- Edit: --
Changed reference from open_file
to open_filex
, since the original package requests REQUEST_INSTALL_PACKAGES rights by default on android. This blocks review in Google Play, from september 2022.
Upvotes: 8
Reputation: 30103
It's hard to get this to work correctly. Here are some hints that helped me to launch ACTION_VIEW
intents from Flutter, with files downloaded with Flutter.
1) Register a FileProvider
in the android manifest:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapp.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
provider_paths.xml
:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
</paths>
2) Create a custom platform channel that provides 2 methods (Code below is Kotlin):
getDownloadsDir
: Should return the data dir where downloaded files should be placed. Try this one:
val downloadsDir = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).path
result.success(downloadsDir);
previewFile
that takes 2 string arguments: path
(File.path
in Dart) and type
(e.g. "application/pdf"):
val file = File(path);
val uri = FileProvider.getUriForFile(this, "com.example.myapp.provider", file);
val viewFileIntent = Intent(Intent.ACTION_VIEW);
viewFileIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION);
viewFileIntent.setDataAndType(uri, type);
try {
startActivity(viewFileIntent);
result.success(null);
} catch (e: ActivityNotFoundException) {
result.error(...);
}
The most important part is the creation of the FileProvider
. url_launcher and android_intent will not work, you have to create your own platform channel. You can change the download path, but then you also have to find the correct provider/permissions settings.
Making this work on iOS is also possible, but out of scope of this question.
If you are using the image_picker plugin:
The FileProvider
conflicts with the current version of the image_picker (0.4.6) plugin, a fix will be released soon.
Upvotes: 9