美文网首页Viewandroid进阶精选案例
Android-自定义应用选择器

Android-自定义应用选择器

作者: JulianAndroid | 来源:发表于2017-06-24 16:09 被阅读2631次

    考虑到很多小伙伴可能没有耐心看完整篇文章,请原谅我厚颜无耻的把项目的地址Sample APK放在最最最前面。

    我司主营企业版云存储服务,在一段时间里经常有用户反馈点击某个文件会自动跳转到系统自带APP(大多是音乐播放器)的问题。一开始我以为可能是小白用户设置了“默认打开方式”,结果不是。
    经过几番沟通,总结出了下面几条规律:

    1. 出现问题的基本是魅族手机,但并不是所有的魅族手机都有这类问题
    2. 并不是点击所有的文件都会跳转到系统自带应用
    3. 出现问题的文件类型包括: .md, .dwg

    于是我向公司申请了一台魅族手机,功夫不负有心人,哈哈😂,重现了这个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 过滤器匹配 Intent,这不仅有助于发现要激活的目标组件,还有助于发现设备上组件集的相关信息。 例如,主页应用通过使用指定 ACTION_MAIN
    操作和 CATEGORY_LAUNCHER
    类别的 Intent 过滤器查找所有 Activity,以此填充应用启动器。
    您的应用可以采用类似的方式使用 Intent 匹配。PackageManager
    提供了一整套 query...()
    方法来返回所有能够接受特定 Intent 的组件。此外,它还提供了一系列类似的 resolve...()
    方法来确定响应 Intent 的最佳组件。 例如,[queryIntentActivities()](https://developer.android.com/reference/android/content/pm/PackageManager.html#queryIntentActivities(android.content.Intent, int))
    将返回能够执行那些作为参数传递的 Intent 的所有 Activity 列表,而 [queryIntentServices()](https://developer.android.com/reference/android/content/pm/PackageManager.html#queryIntentServices(android.content.Intent, int))
    则可返回类似的服务列表。这两种方法均不会激活组件,而只是列出能够响应的组件。 对于广播接收器,有一种类似的方法: [queryBroadcastReceivers()](https://developer.android.com/reference/android/content/pm/PackageManager.html#queryBroadcastReceivers(android.content.Intent, int))。

    冥冥之中感觉[queryIntentActivities()](https://developer.android.com/reference/android/content/pm/PackageManager.html#queryIntentActivities(android.content.Intent, 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);
            }
        }
    }
    

    如果你还不了解单元测试或者认为写单元测试纯粹是浪费时间,不知道上面这段代码能不能成为你写单元测试的理由。如果你不写,为了测试 [queryIntentActivities()](https://developer.android.com/reference/android/content/pm/PackageManager.html#queryIntentActivities(android.content.Intent, int)),你可能需要下面的步骤:

    1. 创建一个Activity
    2. 在onCreate()里写需要测试的代码
    3. Run整个项目,等待......
    4. 跳转页面,找到创建的Activity
    5. 看效果

    如果用单元测试,你只需要运行测试用例静静的等待结束,看结果就好了,下图是我的手机运行上面的测试用例的结果:

    testQueryIntentActivities
    看到结果,我似乎明白了 Context#start...(Intent) 系列方法的工作原理:如果使用的是显式Intent,就直接去启动具体的组件;如果使用的是隐式Intent,那么系统先经过筛选找到所有符合Intent描述信息的组件,然后显示符合条件的组件列表供你选择。其实,隐式Intent最终还是被转换成了显示Intent。

    实现Activity选择器

    经过上面分析,我们就可以开始实现自定义Activity选择器了,这个项目的名称就叫做 AppChooser

    先来看一下效果:

    这个项目实现的功能如下:

    1. 让用户选择Activity打开指定文件
    2. 用户可以设置默认打开方式
    3. 用户可以清空默认打开方式

    下面这张活动图描述了整个过程的基本流程:

    activityChartDiagram

    引入项目

    compile 'io.julian:appchooser:1.0.4'

    使用方法

    在Activity或Fragment中:

    @NonNull
    private AppChooser mAppChooser;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_infos);
        // 初始化 AppChooser
        mAppChooser = AppChooser.with(this); 
    }
    
    @Override
    public void onStart() {
        super.onStart();
        // 绑定 AppChooser
        mAppChooser.bind();
    }
    @Override
    public 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();
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK
                && requestCode == REQUEST_CODE_MODIFY_FILE) {
            // 编辑结果的回调
        }
    }
    

    最后

    如果有兴趣的同学,请转至https://github.com/JulianAndroid/AppChooser ,加个小星星。本人也挺懒的,你的支持是我写博客的动力🙈🙏🏼。

    相关文章

      网友评论

      • 空心菜的爱:Intent.createChooser() 这个会有用吗?
        空心菜的爱:@JulianAndroid 稳
        JulianAndroid:我记得当时找解决办法的时候试过 Intent.createChooser() ,用这个方式可以弹出系统的应用选择器。但有一个问题:不能设置默认打开方式。可以看一下这篇文章(自备梯子):https://developer.android.com/training/sharing/send.html。
        试一下下面的代码:
        Intent sendIntent = new Intent();
        sendIntent.setAction(Intent.ACTION_SEND);
        sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
        sendIntent.setType("text/plain");
        startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_to)));
      • IAM四十二:魅族家的手机真是神一样的存在!:innocent:
        JulianAndroid:@IAM四十二 至于是不是华为手机“特有的问题”现在还需要继续观察,问题如下:有个华为手机的用户下载了一个文件到 ~/Android/data/pkgName/files/ 目录下,过了一段时间需要重新下载。
        不知道有没有遇到过这个问题。目前推测这可能跟华为提供授权机制有关:http://developer.huawei.com/consumer/cn/devunion/download/anquanleikaifazhidaoshuV1.2.0.rar
        IAM四十二: @JulianAndroid 较少
        JulianAndroid:有没有踩过华为的坑?

      本文标题:Android-自定义应用选择器

      本文链接:https://www.haomeiwen.com/subject/yceyzttx.html