Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter: how to let a file be opened by an external app (like Android's implicit intent)?

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:

  1. url_launcher plugin (as suggested in How to open an application from a Flutter app?)
  2. android intent
  3. share plugin

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:

  • Converted uri type from file:// to content:// because I was getting android.os.FileUriExposedException
  • Stored temporary file in external storage because I don't want to deal with granting URI READ PERMISSION just yet. (I tried, but android_intent doesn't have a way to set intent flags just yet)
like image 243
Jefferson Avatar asked Aug 16 '18 21:08

Jefferson


People also ask

How do I open an external app in flutter?

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.

How do I send intent to another app?

Sending binary content Intent shareIntent = new Intent(); shareIntent. setAction(Intent. ACTION_SEND);


1 Answers

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);
  });
}
like image 154
Kristian Avatar answered Sep 28 '22 07:09

Kristian