Flutter:如何让外部应用程序打开文件(例如Android的隐式意图)?

问题描述:

我正在构建一个Flutter应用程序,该应用程序本质上是从云中获取数据的.数据类型各不相同,但它们通常是图像,pdf,文本文件或存档文件(zip文件).

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插件(如中的建议)从Flutter应用中获取?)
  2. android意向
  3. 共享插件

3号路由并不是我真正想要的,因为它使用的是平台的共享"机制(即在Twitter上发布/发送给联系人),而不是打开有效负载.

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.

路线1和2种工作方式……以一种不稳定,怪异的方式.我稍后再解释.

Route 1 & 2 sort of worked... in a shaky, weird way. I'll explain later.

这是我的代码流:

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";

以上代码使用的是 url_launcher 插件.如果我使用的是 android_intent 插件,那么最后几行代码将变为:

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)

当我尝试共享URI时,事情变得很奇怪.我已经在3种不同的手机上测试了这段代码.其中之一(三星Galaxy S9)将抛出此异常:

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)

我不知道 cmp = com.google.android.gm/.browse.TrampolineActivity

此异常仅发生在Galaxy S9中.另外两部手机没有给我这个问题.他们确实启动了文件uri,有人问我如何打开文件,但没有提供任何图像处理应用程序(例如Gallery,QuickPic或Google Photos).

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).

只需澄清一下, url_launcher android_intent 路由都会得出完全相同的结果.

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:

  • 将uri类型从file://转换为content://,因为我收到了 android.os.FileUriExposedException
  • 将临时文件存储在外部存储中,因为我现在还不想处理授予URI READ PERMISSION的问题.(我尝试过,但是 android_intent 尚无法设置意图标志)
  • 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)

很难使其正常工作.以下是一些提示,这些提示帮助我从Flutter启动 ACTION_VIEW 意图,并使用Flutter下载文件.

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)在Android清单中注册 FileProvider :

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)创建一个提供2种方法的自定义平台渠道(以下代码为Kotlin):

2) Create a custom platform channel that provides 2 methods (Code below is Kotlin):

getDownloadsDir :应返回放置已下载文件的数据目录.试试这个:

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 ,它带有2个字符串参数: path (在Dart中为 File.path )和类型(例如"application/pdf"):

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(...);
}

最重要的部分是创建 FileProvider .url_launcher和android_intent无法使用,您必须创建自己的平台频道.您可以更改下载路径,但随后还必须找到正确的提供者/权限设置.

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.

也可以在iOS上进行这项工作,但这超出了这个问题的范围.

Making this work on iOS is also possible, but out of scope of this question.

如果您使用的是image_picker插件:

If you are using the image_picker plugin:

FileProvider 与当前版本的image_picker(0.4.6)插件冲突,将很快发布修复程序.

The FileProvider conflicts with the current version of the image_picker (0.4.6) plugin, a fix will be released soon.