我司主营企业版云存储服务,在一段时间里经常有用户反馈点击某个文件会自动跳转到系统自带APP(大多是音乐播放器)的问题。一开始我以为可能是小白用户设置了“默认打开方式”,结果不是。
经过几番沟通,总结出了下面几条规律:
出现问题的基本是魅族手机,但并不是所有的魅族手机都有这类问题
并不是点击所有的文件都会跳转到系统自带应用
出现问题的文件类型包括:
于是我向公司申请了一台魅族手机,功夫不负有心人,哈哈,重现了这个bug。下面是我们当时创建打开文件的Intent的代码片段:
public static Intent makeOpenFileIntent(Context context, String mime, File path) { Intent intent = new Intent(Intent.ACTION_VIEW); LogUtils.v(TAG, "Open file with mime: " + mime); if (StringUtils.isNullOrEmpty(mime)) { intent.setDataAndType(Uri.fromFile(path), "*/*"); } else { intent.setDataAndType(Uri.fromFile(path), mime); } return intent; }
通过Intent请求系统筛选出能打开目标文件的Activity,基本都是通过上面这段代码来实现的,没毛病。
使用魅族手机debug后发现,出问题的都是那些 mime 为null
的文件。mime这个参数,即文件的 MimeType。通过下面的代码来获取:
MimeTypeMap.getSingleton().getMimeTypeFromExtension(String extension);
由此基本可以得出结论,这些出问题的魅族手机发现你传递过来的文件的 MimeType为 */*
时,并不会弹出所有支持 Intent.ACTION_VIEW
的Activity供你选择,而是直接跳转到某个系统自带的应用了。
经过几番周折,我们去魅族开发者论坛、谷歌、百度始终没有找到一个合理的解决方案。突然,瞬间开了窍,既然我们的软件出了这个问题,别人家的软件要么也有问题,要么没有问题,看看人家是怎么处理的。于是我看了包括:百度网盘、ES文件浏览器,发现这些软件清一色的自定义此功能,都没有使用系统自带的处理方式。看到这里身为一枚Android汪,感觉好无助。
现在终于有了解决问题的方向,即自定义Activity选择器。于是开始Google关键在“Intent”,在阅读 中似乎看到了曙光。原文如下:
通过 Intent 过滤器匹配 Intent,这不仅有助于发现要激活的目标组件,还有助于发现设备上组件集的相关信息。 例如,主页应用通过使用指定
操作和
类别的 Intent 过滤器查找所有 Activity,以此填充应用启动器。
您的应用可以采用类似的方式使用 Intent 匹配。
提供了一整套 query...()
方法来返回所有能够接受特定 Intent 的组件。此外,它还提供了一系列类似的 resolve...()
方法来确定响应 Intent 的最佳组件。 例如,queryIntentActivities())
将返回能够执行那些作为参数传递的 Intent 的所有 Activity 列表,而 queryIntentServices())
则可返回类似的服务列表。这两种方法均不会激活组件,而只是列出能够响应的组件。 对于广播接收器,有一种类似的方法: queryBroadcastReceivers())。
, int))就是我要的滑板鞋。紧接着写了下面这一段单元测试:
@RunWith(AndroidJUnit4.class)public class ResolversRepositoryTest { private static final String TAG = ResolversRepositoryTest.class.getSimpleName(); @Test public void testQueryIntentActivities() throws Exception { File txt = new File("/test.txt"); Uri uri = Uri.fromFile(txt); // 获取扩展名 String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); // 获取MimeType String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); // 创建隐式Intent Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); Context context = InstrumentationRegistry.getContext(); PackageManager packageManager = context.getPackageManager(); // 根据Intent查询匹配的Activity列表 List<ResolveInfo> resolvers = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolver : resolvers) { Log.d(TAG, resolver.activityInfo.name); } } }
, int)),你可能需要下面的步骤:
创建一个Activity
在onCreate()里写需要测试的代码
Run整个项目,等待......
跳转页面,找到创建的Activity
看效果
如果用单元测试,你只需要运行测试用例静静的等待结束,看结果就好了,下图是我的手机运行上面的测试用例的结果:
看到结果,我似乎明白了 Context#start...(Intent)
系列方法的工作原理:如果使用的是显式Intent,就直接去启动具体的组件;如果使用的是隐式Intent,那么系统先经过筛选找到所有符合Intent描述信息的组件,然后显示符合条件的组件列表供你选择。其实,隐式Intent最终还是被转换成了显示Intent。
实现Activity选择器
。
先来看一下效果:
这个项目实现的功能如下:
让用户选择Activity打开指定文件
用户可以设置默认打开方式
用户可以清空默认打开方式
下面这张活动图描述了整个过程的基本流程:
引入项目
compile 'io.julian:appchooser:1.0.4'
使用方法
在Activity或Fragment中:
@NonNullprivate AppChooser mAppChooser;@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_file_infos); // 初始化 AppChooser mAppChooser = AppChooser.with(this); }@Overridepublic void onStart() { super.onStart(); // 绑定 AppChooser mAppChooser.bind(); }@Overridepublic void onStop() { super.onStop(); // 解绑 AppChooser mAppChooser.unbind(); }/** * 打开文件 * * @param file 待打开的文件 */private void showFile(@NonNull File file) { // 检查文件非空 Preconditions.checkNotNull(file); // 必须是文件 Preconditions.checkArgument(file.isFile()); mAppChooser.file(file).load(); }/** * 打开文件并将编辑的结果回传给 Activity 或 Fragment * * @param file 待打开的文件 * @see android.app.Activity#onActivityResult(int, int, Intent) * @see android.support.v4.app.Fragment#onActivityResult(int, int, Intent) */private void modifyFile(@NonNull File file) { // 检查文件非空 Preconditions.checkNotNull(file); // 必须是文件 Preconditions.checkArgument(file.isFile()); mAppChooser.file(file).requestCode(REQUEST_CODE_MODIFY_FILE).load(); }@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_MODIFY_FILE) { // 编辑结果的回调 } }