甲方四个字,让我看了一圈 Android PMS 源码

背景

这几天有一个拍照的需求,只需要简单的调用相机拍照就可以,直接通过intent 就可以完成。但是如果手机安装了多个相机时,会弹窗提示用户选择使用哪个相机,对于这一点,甲方说了四个字

用系统的

1.jpg

好吧,那就干吧。通过搜索,了解到可以添加包名来限制响应我们intent,那我们只要获取系统相机的包名就可以了。接下来我们看下整个流程。

调用相机拍照

这里我们可以构建intent ,新建一个临时文件,通过uri 的形式提供出去,等系统相机拍照完成后就会将图片保存到我们提供的uri 中,接着我们就可以直接使用了。这里面对于用户手机上安装了其它的相机 app,就会弹窗提示用户选择一个相机使用,但是在测试情况中部分第三方相机可以拍照但是照片数据并没有按官方规则的保存到我们传入的url 中。再加上甲方的需求,所以这里我们限制只允许调用系统相机,通过intent.setPackage方法传入系统相机的包名即可。代码如下。

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val cameraPackage = getSystemCameraPackageName(this)
if (cameraPackage.isNullOrEmpty()) {
    ToastUtils.showShort("No System Camera")
    return@setOnClickListener
}
intent.setPackage(cameraPackage)
val saveFileDir = File(this.externalCacheDir, "PhotoUtils")
saveFileDir.mkdirs()
val fileName = fileDateFormat.format(Date()) + ".jpg"
val file = File(saveFileDir, fileName)
val uri = FileProvider.getUriForFile(this, "$packageName.provider", file)
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
startActivityForResult(intent, 2000)
复制代码

限制调用系统相机

接下来就是需要我们获取系统相机的包名了,先介绍下第一版本我使用的方法。首先获取所有安装包信息,然后通过名称判断来获取系统相机的包名。这个方法有点拙劣,但是确实是可行的。具体代码如下,不建议使用。

        val packages = context.packageManager.getInstalledPackages(0)
        for (i in packages.indices) {
            val packageInfo = packages[i]
            val strLabel = packageInfo.applicationInfo.loadLabel(context.packageManager).toString()
            // 一般手机系统中拍照软件的名字
            if ("相机,照相机,照相,拍照,摄像,Camera,camera".contains(strLabel)) {
                systemCameraPackageName = packageInfo.packageName
                if (packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
                    break
                }
            }
        }
复制代码

在准备将这个功能抽离出来的时候,看到这段代码我决定看看有没有更好的方法,最终在 stackoverflow 上面看到一个回答中提到的,代码如下。

public static ResolveInfo  getCameraPackageName(Context context, PackageManager pm) {
    Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
    ResolveInfo cameraInfo = null;
    List<ResolveInfo> pkgList = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
    if(pkgList != null && pkgList.size() > 0) {
        cameraInfo = pkgList.get(0);
    }
    return(cameraInfo);
}
复制代码

在测试后发现这种方法也是可以的,当然如果我们手机上有多个相机,pkgList 返回的就会有多条数据。这里面直接取第一条就可以了。到这里我们就可以调用相机拍照,且可以限制只调用系统相机。

安装多个相机时可行吗?

结论

上面的cameraInfo = pkgList.get(0); 我们直接取的第一条,当这里面有多条时直接取第一条是可行的吗?根据我的分析结果是可行的。(如有错误,请各位指正。)我们直接进入源码来看看。

源码

我们代码中构造了一个拍照的意图,然后进行搜索,这样返回的都是可以响应我们这个意图的应用信息。我们通过getPackageManager(); 获取 PackageManager 这里面的方法是在Context 中,我们找到实现类ContextImpl 中的方法。

    @Override
    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }

        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // 可以看到我们获取到的是 ApplicationPackageManager
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }

        return null;
    }
复制代码

那我们调用的就是ApplicationPackageManager.queryIntentActivities方法,继续看下去。接着调用了queryIntentActivitiesAsUser 方法。继续来到mPM.queryIntentActivities 方法,可以发现是通过PackageManagerService 服务返回的信息,我们在PMS中找到对应的代码。这里只摘录部分代码如下。

List<ResolveInfo> result = mActivities.queryIntent(intent, resolvedType, flags, userId);
if (resolveInfo != null) {
    result.add(resolveInfo);
    Collections.sort(result, mResolvePrioritySorter);
}
复制代码

mActivitiesActivityIntentResolver 类,我们继续看,这里会调用到父类IntentResolverqueryIntent方法,这里面的条件处理比较多,因为需要根据不同情况来匹配,这里因为我们没有设置其它条件,所以会走到语句 firstTypeCut = mActionToFilter.get(intent.getAction());

if (resolvedType == null && scheme == null && intent.getAction() != null) {
  firstTypeCut = mActionToFilter.get(intent.getAction());
  if (debug) Slog.v(TAG, "Action list: " + Arrays.toString(firstTypeCut));
}

//...
if (firstTypeCut != null) {
  buildResolveList(intent, categories, debug, 
  defaultOnly,resolvedType, scheme, firstTypeCut, finalList, userId);
}
复制代码

我们看下mActionToFilter 是什么,是一个集合,集合里面保存所有注册的Action ,这样我们就可以匹配到我们需要的ACTION_IMAGE_CAPTURE 然后返回,再通过进一步的处理就得到了我们在上面获取到的List<ResolveInfo> pkgList

/**
* All of the actions that have been registered, but only those that did
* not specify data.
*/
private final ArrayMap<String, F[]> mActionToFilter = new ArrayMap<String, F[]>();
复制代码

这里面保存的肯定是系统内所有的类型数据,那么应该就会是在PMS 内赋值的,因为PMS 负责解析这些数据,那顺序应该也会是frawework app->system app->user app 所以这里面我们直接获取最后结果列表中的第一个似乎并没有什么问题。

PMS 解析过程

上面的结论都是我们的推测,接下来我们继续看下PMS 部分代码来一探究竟。先看到PMS 的构造函数,构造函数内会调用scanDirLI 方法来扫描所有的安装包。顺序是从framework,system,oem,app 依次进行。scanDirLI 方法内又会调用第一个scanPackageLI(File scanFile, int parseFlags, int scanFlags, long currentTime, UserHandle user) 方法。接着调用scanPackageLI(PackageParser.Package pkg, int parseFlags, int scanFlags, long currentTime, UserHandle user)注意scanPackageLI 这个方法调用了两次,是不同参数的重载函数。然后第二次调用的scanPackageLI 内会调用scanPackageDirtyLI() 方法,这个函数内有句代码需要我们关注下,就是mActivities.addActivity(a, "activity"); 这里面的mActivitiesActivityIntentResolver 类型的,父类型是IntentResolveraddActivity 方法又会来到父类的addFilter(intent); 方法。这个方法内就会将我们的intent 保存到mActionToFilter 也就是我们上面的检索的集合。这样我们在使用的时候就可以直接检索出数据了,这也就是PMS 为我们做的工作之一。这一段具体的代码逻辑很多,但是由于我们的关注点在这个mActionToFilter 所以很多部分就略过了。

    public void addFilter(F f) {
        //...
        if (numS == 0 && numT == 0) {
            register_intent_filter(f, f.actionsIterator(), mActionToFilter, "      Action: ");
        }
        //...
    }
复制代码

总结

看到这里,我们的推测应该初步成立了。那么我们的需求功能也就能够完成了。虽然功能很普通,但是追踪源码的验证方法还是值得多看几遍的,多看几遍后对于这个解析过程也会有点新的理解。

好了,看来今天能正常下班了。

おすすめ

転載: juejin.im/post/7031348317023895589