如何拿到插件Apk的Intent Filter?

加载插件时需要获取到插件APK的详细信息,比如AndroidManifest.xml中注册的四大组件。Android SDK提供了接口,如下:

PackageManager pm = getPackageManager();

PackageInfo packageInfo = pm.getPackageArchiveInfo(
        apkFile.getAbsolutePath(), PackageManager.GET_ACTIVITIES
                | PackageManager.GET_SERVICES
                | PackageManager.GET_PROVIDERS
                | PackageManager.GET_RECEIVERS
                | PackageManager.GET_META_DATA);

ActivityInfo[] activityInfos = packageInfo.activities;
ApplicationInfo applicationinfo = packageInfo.applicationInfo;
Bundle metaData = applicationinfo.metaData;
ServiceInfo[] serviceInfos = packageInfo.services;
ProviderInfo[] providerInfos = packageInfo.providers;
ActivityInfo[] receiverInfos = packageInfo.receivers;

我们重点关注一下Activity,发现ActivityInfo中基本的信息都有,包括name、processName、theme、configChanges、launchMode等,不过缺少一个非常关键的东西,那就是Intent-Filter。

为什么我会专门提到这个呢?如果我们启动插件中的组件时直接指定类名,那确实不用理会Intent-Filter了,直接按name查找组件就OK了。比如我们目前使用的插件框架,调起插件Apk中的Activity都是通过指定Activity的类名来查找对应的Activity并将其当做普通类调起,然后在一个Proxy Activity中调用其生命周期的回调。如果要调起插件Apk中的Launcher Activity,宿主APP是无法知道谁是插件的Launcher Activity的,因为我们拿不到Intent-Filter。解决办法是在插件的AndroidManifest.xml的meta-data中声明一个专用类,宿主APP会调用该类的特定函数来启动Launcher Activity,由于是插件内部类调的,所以插件自己当然知道哪个是Launcher Activity,宿主就不用操心了。这一系列做法虽然能解决问题,不过总显得很累赘和不自然,别人为了做插件需要额外增加不少工作量,而本文就是为了解决这一系列问题提出一些思路。

这里核心问题是如何在SDK未提供相应接口的情况下获取Activity的Intent Filter。

我们先看看pm.getPackageArchiveInfo的实现,或许其中可以发现点什么:

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) {
    final PackageParser parser = new PackageParser();
    final File apkFile = new File(archiveFilePath);
    try {
        PackageParser.Package pkg = parser.parseMonolithicPackage(apkFile, 0);
        PackageUserState state = new PackageUserState();
        return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0, null, state);
    } catch (PackageParserException e) {
        return null;
    }
}

这里先新建一个PackageParser,调用parseMonolithicPackage生成Package,然后调用其静态函数generatePackageInfo来解析并生成PackageInfo。值得一提的是这个parseMonolithicPackage用于解析单独的apk文件,如果是apk的目录则用parseClusterPackage函数解析。在PackageParser中有一个函数会自动根据apk文件类型来分别调用对应的解析函数,如下:

public Package parsePackage(File packageFile, int flags) throws PackageParserException {
    if (packageFile.isDirectory()) {
        return parseClusterPackage(packageFile, flags);
    } else {
        return parseMonolithicPackage(packageFile, flags);
    }
}

接下来我们看看parseMonolithicPackage的实现:

public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
    final AssetManager assets = new AssetManager();
    try {
        final Package pkg = parseBaseApk(apkFile, assets, flags);
        pkg.codePath = apkFile.getAbsolutePath();
        return pkg;
    } finally {
        IoUtils.closeQuietly(assets);
    }
}

可见这里核心是调用了parseBaseApk,继续看下去:

private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
            String[] outError) throws XmlPullParserException, IOException {
    ..........

    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
            continue;
        }

        String tagName = parser.getName();
        if (tagName.equals("application")) {
            ..........

            if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) {
                return null;
            }
        } 
        ..........
    }

    ..........
}

这里开始解析apk中的AndroidManifest.xml文件了,重点是看application标签的解析,调用的是parseBaseApplication,四大组件都在这里面解析的。

private boolean parseBaseApplication(Package owner, Resources res,
            XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
    throws XmlPullParserException, IOException {
    ..........

    final int innerDepth = parser.getDepth();

    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
            continue;
        }

        String tagName = parser.getName();
        if (tagName.equals("activity")) {
            Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,
                    owner.baseHardwareAccelerated);

            owner.activities.add(a);

        } else if (tagName.equals("receiver")) {
            Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false);

            owner.receivers.add(a);

        } else if (tagName.equals("service")) {
            Service s = parseService(owner, res, parser, attrs, flags, outError);

            owner.services.add(s);

        } else if (tagName.equals("provider")) {
            Provider p = parseProvider(owner, res, parser, attrs, flags, outError);

            owner.providers.add(p);

        } else if (tagName.equals("activity-alias")) {
            Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError);

            owner.activities.add(a);

        } else if (parser.getName().equals("meta-data")) {
            owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData,
                    outError)
        } 
        ..........
    }

    ..........

    return true;
}

这里省略了不少代码,不过核心部分非常鲜明,就是解析xml中的标签,我们重点关注activity:

private Activity parseActivity(Package owner, Resources res,
        XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,
        boolean receiver, boolean hardwareAccelerated)
        throws XmlPullParserException, IOException {
    .........

    Activity a = new Activity(mParseActivityArgs, new ActivityInfo());

    .........

    int outerDepth = parser.getDepth();
    int type;
    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
           && (type != XmlPullParser.END_TAG
                   || parser.getDepth() > outerDepth)) {
        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
            continue;
        }

        if (parser.getName().equals("intent-filter")) {
            ActivityIntentInfo intent = new ActivityIntentInfo(a);
            if (!parseIntent(res, parser, attrs, true, true, intent, outError)) {
                return null;
            }
            if (intent.countActions() > 0) {
                a.intents.add(intent);
            }
        }
        .........
    }

    return a;
}

private boolean parseIntent(Resources res, XmlPullParser parser, AttributeSet attrs,
            boolean allowGlobs, boolean allowAutoVerify, IntentInfo outInfo, String[] outError)
            throws XmlPullParserException, IOException {
    ..........

    int outerDepth = parser.getDepth();

    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {

        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
            continue;
        }

        String nodeName = parser.getName();
        if (nodeName.equals("action")) {
            String value = attrs.getAttributeValue(
                    ANDROID_RESOURCES, "name");

            outInfo.addAction(value);
        } else if (nodeName.equals("category")) {
            String value = attrs.getAttributeValue(
                    ANDROID_RESOURCES, "name");

            outInfo.addCategory(value);
        } 
        ..........
    }

    ..........

    return true;
}

可见这里解析到了Intent Filter,并添加到了Activity的intents中。那为什么最终我们获取不到Intent Filter呢?我们来看看这些数据结构,先看看Activity:

public final static class Activity extends Component<ActivityIntentInfo> {
    public final ActivityInfo info;
    ..........
}

这个类是PackageParser的静态内部类,是继承自Component,里面只有一个ActivityInfo,这个很眼熟,就是最终开放给我们的数据结构,里面是没有Intent Filter的,看来都在这个Component里。

public static class Component<II extends IntentInfo> {
    public final Package owner;
    public final ArrayList<II> intents;
    public final String className;
    public Bundle metaData;

    ComponentName componentName;
    String componentShortName;
    ..........
}

果然Intent Filter在这里面,是个List,刚传入的类型是ActivityIntentInfo,如下:

public final static class ActivityIntentInfo extends IntentInfo {
    public final Activity activity;
    ..........
}

public static class IntentInfo extends IntentFilter {
    public boolean hasDefault;
    public int labelRes;
    public CharSequence nonLocalizedLabel;
    public int icon;
    public int logo;
    public int banner;
    public int preferred;
}

public class IntentFilter implements Parcelable {
    private int mPriority;
    private final ArrayList<String> mActions;
    private ArrayList<String> mCategories = null;
    private ArrayList<String> mDataSchemes = null;
    private ArrayList<PatternMatcher> mDataSchemeSpecificParts = null;
    private ArrayList<AuthorityEntry> mDataAuthorities = null;
    private ArrayList<PatternMatcher> mDataPaths = null;
    private ArrayList<String> mDataTypes = null;

    private int mVerifyState;
    ..........
}

好了,这么多数据结构,我们总结一下,Activity继承自Component,Component里面有个ActivityIntentInfo的列表,这个ActivityIntentInfo是继承自IntentFilter,里面有我们关注的Intent Filter的mActions和mCategories。

接下来问题来了,既然解析Apk的时候是有这些Intent Filter的,并且保存到了Activity中,这些Activity又保存到了PackageParser.Package中,为什么最终我们获取不到呢?关键是下面这个函数,将解析出来的Package作为参数,返回的是PackageInfo,大概明白了,虽然Package中什么信息都有了,但是最终返给我们的是一个阉割版的PackageInfo。

public static PackageInfo generatePackageInfo(PackageParser.Package p,
        int gids[], int flags, long firstInstallTime, long lastUpdateTime,
        HashSet<String> grantedPermissions, PackageUserState state, int userId) {
    PackageInfo pi = new PackageInfo();

    ..........

    if ((flags & PackageManager.GET_GIDS) != 0) {
        pi.gids = gids;
    }

    if ((flags & PackageManager.GET_CONFIGURATIONS) != 0) {
        ..........
    }

    if ((flags & PackageManager.GET_ACTIVITIES) != 0) {
        int N = p.activities.size();

        ..........

        for (int i = 0, j = 0; i < N; i++) {
            final Activity activity = p.activities.get(i);
            pi.activities[j++] = generateActivityInfo(p.activities.get(i), flags,
                    state, userId);
        }
    }
    if ((flags & PackageManager.GET_RECEIVERS) != 0) {
        ..........
    }
    if ((flags & PackageManager.GET_SERVICES) != 0) {
        ..........
    }
    if ((flags & PackageManager.GET_PROVIDERS) != 0) {
        ..........
    }
    if ((flags & PackageManager.GET_INSTRUMENTATION) != 0) {
        ..........
    }
    if ((flags & PackageManager.GET_PERMISSIONS) != 0) {
        ..........
    }
    if ((flags & PackageManager.GET_SIGNATURES) != 0) {
        ..........
    }
    return pi;
}

我们还是重点看看Activity的逻辑,generateActivityInfo是如何阉割Package中的Activity的:

public static final ActivityInfo generateActivityInfo(Activity a, int flags,
            PackageUserState state, int userId) {
    if (!copyNeeded(flags, a.owner, state, a.metaData, userId)) {
        return a.info;
    }
    ActivityInfo ai = new ActivityInfo(a.info);
    ai.metaData = a.metaData;
    ai.applicationInfo = generateApplicationInfo(a.owner, flags, state, userId);
    return ai;
}

可见这里就是直接返回了Activity中的ActivityInfo,而Intent Filter就这样被阉割掉了。虽然很无奈,不过没关系,接下来我们想办法怎么给这个Intent Filter再找回来。不过遗憾的是这个Package貌似没有全局缓存起来,解析完就丢掉了。这么说如果我们要重新获取到Package的话只能再解析一遍了,调用的就是我们上面提到的PackageParser的parsePackage函数。

首先我们需要获取一个PackageParser,参考getPackageArchiveInfo的做法,直接new一个就好了,不过这里我们获取不到这个类,只能通过反射了。获取到PackageParser后,调用其parsePackage函数,传入apk路径,flags设为0即可。如下:

private void parseApk(File apkFile) {
    try {
        Class<?> clazz = Class.forName("android.content.pm.PackageParser");
        Object packageParser = MethodUtils.invokeConstructor(clazz, apkFile.getAbsolutePath());
        Object packageObj = MethodUtils.invokeMethod(packageParser, "parsePackage", apkFile, apkFile.getAbsolutePath(), 0);
        List activies = (List) FieldUtils.readField(packageObj, "activities");
        for (Object data : activies) {
            List<IntentFilter> filters = (List) FieldUtils.readField(data, "intents");
            for (IntentFilter filter : filters) {
                for (int i = 0; i < filter.countActions(); i++) {
                    Log.i("bush", "filter: " + filter.getAction(i));
                }

                for (int i = 0; i < filter.countCategories(); i++) {
                    Log.i("bush", "filter: " + filter.getCategory(i));
                }
            }
        }
    } catch (ClassNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (InstantiationException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

不过这里要注意的是适配不同sdk版本,parsePackage参数可能不一样。

好了,到这里我们的目的就算达到了,拿到了Intent Filter,调起apk的时候就可以直接启动Launcher Activity了。此外,我们可以直接通过Intent而不是指定类名来启动插件中的Activity了,因为我们可以自己去resolve这个intent从而在各个插件apk中找到最匹配的Activity。

你可能感兴趣的:(android,插件)