Android-自定义应用选择器

考虑到很多小伙伴可能没有耐心看完整篇文章,请原谅我厚颜无耻的把项目的地址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 ,加个小星星。本人也挺懒的,你的支持是我写博客的动力🙈🙏🏼。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容