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)For opening an external app from your app in android, you need provide packageName of the app. If the plugin finds the app in the device, it will be be launched. But if the the app is not installed in the device then it leads the user to playstore link of the app.
Sending binary content Intent shareIntent = new Intent(); shareIntent. setAction(Intent. ACTION_SEND);
Since this was answered, an excellent flutter plugin open_file 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_file/open_file.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) {
OpenFile.open(path);
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With