ContentProvider plugin

ContentProvider plugin

29 January 2018

ContentProvider 插件化,做起来也很简单。

但是,探索 这个 结论 的过程是 十分复杂 的。

如果往深入的看的话,可以看到 Android 系统的启动过程 以及 App 的安装过程。因为在 这两个点上,都存在 ContentProvider 的安装过程




场景


一般我们 IPC 通信都用 Binder 去完成。但是,Binder 也有 数据大小的限制1 M 缓存。大于 1 M 就会抛出 TransactionTooLargeException

和其他三大组件不一样,ContentProvider 用了 匿名共享内存( Ashmem )完成数据共享。这种方式,就可以解决 Binder 1 M 缓存的问题了。

Android 系统的 消息通讯录 以及 相册 都用 ContentProvider 做的。不然,这些 App 都是一个个的进程,我们怎么在我们的进程访问到它们所在进程的数据呢?




源码小记


当前进程获取


ContentProvider 的获取,是先从 当前进程寻找 ContentProvider 的 远程 Binder Proxy 类( IContentProvider )。找不到的话,就从去访问 AMS 所在的 systemserver 进程,让 AMS 去再次寻找 ContentProvider 的 远程 Binder Proxy 类( IContentProvider )


当前进程安装


如果 ContentProvider 没被安装。那么,会通过 Classloader 加载 ContentProvider class。然后,取出 ContentProvider 对应的 远程 Binder Proxy 类( IContentProvider ),缓存在 ActivityThread # ArrayMap<ProviderKey, ProviderClientRecord> mProviderMap


AMS 所在进程获取


上面 ContentProvider 获取 说到,会去访问 AMS 所在的 systemserver 进程,让 AMS 去再次寻找 ContentProvider 的 远程 Binder Proxy 类( IContentProvider )。那么在 AMS 中,会去访问 PMS让 PMS 去搜索这个 ContentProvider 的设置信息( PackageSetting )

然而,在 PMS 中,缓存 ContentProvider 的设置信息( PackageSetting )的集合,竟然是在 Android 系统启动的时候收集的... 这里,就涉及到序章所说的 Android 系统的启动过程

Android 系统的启动过程。对于 ContentProvider 这块的工作就是,扫描 apk 安装目录,找到 apk 文件,读取 AndroidManifest 中的 provider 标签内容( 也用到了 android.content.pm.PackageParser # generateProviderInfo(...) ),缓存到 PMS 中。不止如此,安装 App 的时候,也会对 ContentProvider 这块也会进行,一样的工作,也涉及到序章所说的 App 的安装过程


AMS 所在进程安装


1. 如果 ContentProvider 所在进程 还在 运行中AMS 会向通过该 ContentProvider 所在进程 的 Binder Proxy 类( ApplicationThread ),发消息回去。最终调用 ActivtyThread # installProvider(...),在该 ContentProvider 所在进程 中完成安装。

2. 如果 ContentProvider 所在进程死亡AMS 会通知 Zygote 进程 fork 出一个 子进程,然后这个 fork 出来的 子进程 执行 ActivityThread # main(...) ,这里的 子进程 就是 ContentProvider 所在进程,可以理解为 fork 出一个 ContentProvider 所在进程。然后 AMS 就进入一个 死循环,等待 fork 出的 ContentProvider 所在进程,完成 ContentProvider 安装。安装完后,这个 fork 出的 ContentProvider 所在进程 会告诉 AMSAMS 结束循环,去告诉 需要这个 ContentProvider 的进程

2.1 比如,进程 A 需要 ContentProviderBContentProviderB 的所在的 进程 B 死亡AMS 会通知 Zygote 进程 fork 出一个 子进程,作为 进程 B。然后执行 B 进程ActivityThread # main(...) 。完成 fork 出一个 进程 B,并执行了 ActivityThread # main(...) 后,AMS 进入 死循环,等待 进程 B 安装完 ContentProviderB进程 B 安装完 ContentProviderB 后,告诉 AMS。最后, AMS 再告诉 进程 A




要点


1. ContentProvider 是用来 共享数据 的,基本用于 增删改查,类似于 HTTP 无状态 服务。

2. ContentProvider 没有生命周期的概念,不想 ActivityService 那样。

3. ContentProvider 进程,要么活着,要么死了。死了,也会被 AMS 通知 Zygote 进程 fork 出一个 子进程,然后启动 ActivityThread # main(...),再安装 ContentProvider

4. Binder1 M 缓存区大小的限制,不然会抛异常。Socket 的话,得一段一段读取内容,也得自己搭协议。ContentProvider 采用了 匿名共享内存,实现文件共享。

5. 四大组件中,除了 ContentProvider 采用了 匿名共享内存( Ashmem )ActivityService 以及 BroadcastReceiver 都是采用 Binder

6. ActivityThread # handBindApplication(...) 方法中,可以发现,ContentProvider 安装启动Application # onCreate(...) 还早。

7. ContentProvider 优先查询本进程是否存在该 ContentProvider 对应的 远程 Binder Proxy 类( IContentProvider )。这样使得,它不需要 hook AMS




ContentProvider plugin 思路


如果想让 别的 App 访问,本 AppContentProvider。就得在 AndroidManifest 注册。
插件没在 AndroidManifest 注册。所以,就算安装了,别的 App 是访问不到了

插桩 ContentProvider 的意义在于,为了让 别的 App 访问得到。
需要一个 灵媒,让 别的 App 能访问的到,这就是 插桩 ContentProvider
然后,再在 插桩 ContentProvider 上进行 分发代理,分发到 插件 ContentProvider


1. 准备一个 插桩 ContentProvider,在内部进行 插件 ContentProvider 协议 的抽取。向 插桩 ContentProvider 发 送的协议,需要组合协议。协议内容得带上 插件 ContentProvider 协议。比如,插件 ContentProvider 协议content://com.camnter.content.provider.plugin.plugin.PluginContentProvider;那么,插桩 ContentProvider 协议content://com.camnter.content.provider.plugin.host.StubContentProvider/ com.camnter.content.provider.plugin.plugin.PluginContentProvider

2. AndroidManifest 内注册 插桩 ContentProvider

3. android.content.pm.PackageParser # generateProviderInfo(...) 读取 apk 文件内的 插件 ContentProvider 信息。

4. 反射调用 ActivityThread # installContentProviders(...) 安装 插件 ContentProvider本进程




ContentProvider plugin


StubContentProvider

/**
 * @author CaMnter
 */

public class StubContentProvider extends ContentProvider {

    public static final Uri STUB_URI = Uri.parse(
        "content://com.camnter.content.provider.plugin.host.StubContentProvider");

    public static final String AUTHORITY
        = "com.camnter.content.provider.plugin.host.StubContentProvider";


    @Override
    public boolean onCreate() {
        return true;
    }


    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        final Uri pluginUri = this.getPluginUri(uri);
        final Context context = this.getContext();
        if (pluginUri == null || context == null) {
            return null;
        }
        return context.getContentResolver()
            .query(pluginUri, projection, selection, selectionArgs, sortOrder);
    }


    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }


    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        final Uri pluginUri = this.getPluginUri(uri);
        final Context context = this.getContext();
        if (pluginUri == null || context == null) {
            return null;
        }
        return context.getContentResolver().insert(pluginUri, values);
    }


    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        final Uri pluginUri = this.getPluginUri(uri);
        final Context context = this.getContext();
        if (pluginUri == null || context == null) {
            return 0;
        }
        return context.getContentResolver().delete(pluginUri, selection, selectionArgs);
    }


    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }


    /**
     * 插件 uri
     * content://com.camnter.content.provider.plugin.plugin.PluginContentProvider
     *
     * 实际 uri 得写成
     * content://com.camnter.content.provider.plugin.host.StubContentProvider/com.camnter.content.provider.plugin.plugin.PluginContentProvider
     *
     * 发给插桩 ContentProvider
     *
     * @param rawUri rawUri
     * @return plugin uri
     */
    @Nullable
    private Uri getPluginUri(@NonNull final Uri rawUri) {
        final String rawAuthority = rawUri.getAuthority();
        if (!AUTHORITY.equals(rawAuthority)) {
            return null;
        }

        return Uri.parse(rawUri.toString().replaceAll(rawAuthority + '/', ""));
    }

}


ProviderInfoUtils

/**
 * @author CaMnter
 */

@SuppressWarnings("DanglingJavadoc")
public final class ProviderInfoUtils {

    private static final String TAG = ProviderInfoUtils.class.getSimpleName();


    /**
     * 解析 Apk 文件中的 <provider>
     * 并缓存
     *
     * 主要 调用 PackageParser 类的 generateProviderInfo 方法
     *
     * @param apkFile apkFile
     * @throws Exception exception
     */
    @SuppressWarnings("unchecked")
    @SuppressLint("PrivateApi")
    public static Map<ComponentName, ProviderInfo> getProviderInfos(@NonNull final File apkFile,
                                                                    @NonNull final Context context)
        throws Exception {

        final Map<ComponentName, ProviderInfo> providerInfoMap = new HashMap<>();

        /**
         * 反射 获取 PackageParser # parsePackage(File packageFile, int flags)
         */
        final Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");

        /**
         * <= 4.0.0
         *
         * Don't deal with
         *
         * >= 4.0.0
         *
         * parsePackage(File sourceFile, String destCodePath, DisplayMetrics metrics, int flags)
         *
         * ---
         *
         * >= 5.0.0
         *
         * parsePackage(File packageFile, int flags)
         *
         */
        final int sdkVersion = Build.VERSION.SDK_INT;
        if (sdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            throw new RuntimeException(
                "[" + TAG + "]   the sdk version must >= 14 (4.0.0)");
        }

        final Object packageParser;
        final Object packageObject;
        final Method parsePackageMethod;

        if (sdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
            // >= 5.0.0
            // parsePackage(File packageFile, int flags)
            /**
             * 反射创建 PackageParser 对象,无参数构造
             *
             * 反射 调用 PackageParser # parsePackage(File packageFile, int flags)
             * 获取 apk 文件对应的 Package 对象
             */
            packageParser = packageParserClass.newInstance();

            parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage",
                File.class, int.class);
            packageObject = parsePackageMethod.invoke(
                packageParser,
                apkFile,
                PackageManager.GET_PROVIDERS
            );
        } else {
            // >= 4.0.0
            // parsePackage(File sourceFile, String destCodePath, DisplayMetrics metrics, int flags)
            /**
             * 反射创建 PackageParser 对象,PackageParser(String archiveSourcePath)
             *
             * 反射 调用 PackageParser # parsePackage(File sourceFile, String destCodePath, DisplayMetrics metrics, int flags)
             * 获取 apk 文件对应的 Package 对象
             */
            final String apkFileAbsolutePath = apkFile.getAbsolutePath();
            packageParser = packageParserClass.getConstructor(String.class)
                .newInstance(apkFileAbsolutePath);

            parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage",
                File.class, String.class, DisplayMetrics.class, int.class);
            packageObject = parsePackageMethod.invoke(
                packageParser,
                apkFile,
                apkFile.getAbsolutePath(),
                context.getResources().getDisplayMetrics(),
                PackageManager.GET_PROVIDERS
            );
        }

        if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            // >= 4.2.0
            // generateProviderInfo(Provider p, int flags, PackageUserState state, int userId)
            /**
             * 读取 Package # ArrayList<Provider> providers
             * 通过 ArrayList<Provider> providers 获取 Provider 对应的 ProviderInfo
             */
            final Field providersField = packageObject.getClass().getDeclaredField("providers");
            final List providers = (List) providersField.get(packageObject);

            /**
             * 反射调用 UserHandle # static @UserIdInt int getCallingUserId()
             * 获取到 userId
             *
             * 反射创建 PackageUserState 对象
             */
            final Class<?> packageParser$ProviderClass = Class.forName(
                "android.content.pm.PackageParser$Provider");
            final Class<?> packageUserStateClass = Class.forName(
                "android.content.pm.PackageUserState");
            final Class<?> userHandler = Class.forName("android.os.UserHandle");
            final Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
            final int userId = (Integer) getCallingUserIdMethod.invoke(null);
            final Object defaultUserState = packageUserStateClass.newInstance();

            // 需要调用 android.content.pm.PackageParser#generateProviderInfo(Provider p, int flags, PackageUserState state, int userId)
            final Method generateProviderInfo = packageParserClass.getDeclaredMethod(
                "generateProviderInfo",
                packageParser$ProviderClass, int.class, packageUserStateClass, int.class);

            /**
             * 反射调用 PackageParser # generateProviderInfo(Provider p, int flags, PackageUserState state, int userId)
             * 解析出 Provider 对应的 ProviderInfo
             *
             * 然后保存
             */
            for (Object provider : providers) {
                final ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(
                    packageParser,
                    provider,
                    0,
                    defaultUserState,
                    userId
                );
                providerInfoMap.put(new ComponentName(info.packageName, info.name), info);
            }
        } else if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
            // >= 4.1.0
            // generateProviderInfo(Provider p, int flags, boolean stopped, int enabledState, int userId)
            /**
             * 读取 Package # ArrayList<Provider> providers
             * 通过 ArrayList<Provider> providers 获取 Provider 对应的 ProviderInfo
             */
            final Field providersField = packageObject.getClass().getDeclaredField("providers");
            final List providers = (List) providersField.get(packageObject);

            // 需要调用 android.content.pm.PackageParser#generateProviderInfo(Provider p, int flags, boolean stopped, int enabledState, int userId)
            final Class<?> packageParser$ProviderClass = Class.forName(
                "android.content.pm.PackageParser$Provider");
            final Class<?> userHandler = Class.forName("android.os.UserId");
            final Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
            final int userId = (Integer) getCallingUserIdMethod.invoke(null);
            final Method generateProviderInfo = packageParserClass.getDeclaredMethod(
                "generateProviderInfo",
                packageParser$ProviderClass, int.class, boolean.class, int.class, int.class);

            /**
             * 反射调用 PackageParser # generateProviderInfo(Provider p, int flags, boolean stopped, int enabledState, int userId)
             * 解析出 Provider 对应的 ProviderInfo
             *
             * 在之前版本的 4.0.0 中 存在着
             * public class PackageParser {
             *     public final static class Package {
             *         // User set enabled state.
             *         public int mSetEnabled = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
             *
             *         // Whether the package has been stopped.
             *         public boolean mSetStopped = false;
             *     }
             * }
             *
             * 然后保存
             */
            for (Object provider : providers) {
                final ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(
                    packageParser,
                    provider,
                    0,
                    false,
                    PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                    userId
                );
                providerInfoMap.put(new ComponentName(info.packageName, info.name), info);
            }
        } else if (sdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            // >= 4.0.0
            // generateProviderInfo(Provider p, int flags)
            /**
             * 读取 Package # ArrayList<Provider> providers
             * 通过 ArrayList<Provider> providers 获取 Provider 对应的 ProviderInfo
             */
            final Field providersField = packageObject.getClass().getDeclaredField("providers");
            final List providers = (List) providersField.get(packageObject);

            // 需要调用 android.content.pm.PackageParser#generateProviderInfo(Provider p, int flags)
            final Class<?> packageParser$ProviderClass = Class.forName(
                "android.content.pm.PackageParser$Provider");
            final Method generateProviderInfo = packageParserClass.getDeclaredMethod(
                "generateProviderInfo",
                packageParser$ProviderClass, int.class);

            /**
             * 反射调用 PackageParser # generateProviderInfo(Provider p, int flags)
             * 解析出 Provider 对应的 ProviderInfo
             *
             * 然后保存
             */
            for (Object provider : providers) {
                final ProviderInfo info = (ProviderInfo) generateProviderInfo.invoke(
                    packageParser,
                    provider,
                    0
                );
                providerInfoMap.put(new ComponentName(info.packageName, info.name), info);
            }
        }

        return providerInfoMap;

    }

}


SmartApplication

/**
 * @author CaMnter
 */

public class SmartApplication extends Application {

    final Map<ComponentName, ProviderInfo> providerInfoMap = new HashMap<>();


    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     *
     * @param base The new base context for this wrapper.
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        try {
            // assets 的 content-provider-plugin-plugin.apk 拷贝到 /data/data/files/[package name]
            AssetsUtils.extractAssets(base, "content-provider-plugin-plugin.apk");
            final File apkFile = getFileStreamPath("content-provider-plugin-plugin.apk");
            final File odexFile = getFileStreamPath("content-provider-plugin-plugin.odex");
            // Hook ClassLoader, 让插件中的类能够被成功加载
            BaseDexClassLoaderHooker.patchClassLoader(this.getClassLoader(), apkFile, odexFile);
            // 解析 provider
            providerInfoMap.putAll(ProviderInfoUtils.getProviderInfos(apkFile, base));
            // 该进程安装 ContentProvider
            this.installProviders(base, providerInfoMap);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    /**
     * 反射 调用 ActivityThread # installContentProviders(Context context, List<ProviderInfo> providers)
     * 安装 ContentProvider
     *
     * @throws NoSuchMethodException NoSuchMethodException
     * @throws ClassNotFoundException ClassNotFoundException
     * @throws IllegalAccessException IllegalAccessException
     * @throws InvocationTargetException InvocationTargetException
     */
    private void installProviders(@NonNull final Context context,
                                  @NonNull final Map<ComponentName, ProviderInfo> providerInfoMap)
        throws NoSuchMethodException,
               ClassNotFoundException,
               IllegalAccessException,
               InvocationTargetException {

        List<ProviderInfo> providerInfos = new ArrayList<>();

        // 修改 ProviderInfo # String packageName
        for (Map.Entry<ComponentName, ProviderInfo> entry : providerInfoMap.entrySet()) {
            final ProviderInfo providerInfo = entry.getValue();
            providerInfo.applicationInfo.packageName = context.getPackageName();
            providerInfos.add(providerInfo);
        }

        if (providerInfos.isEmpty()) {
            return;
        }

        final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        final Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(
            "currentActivityThread");
        final Object currentActivityThread = currentActivityThreadMethod.invoke(null);
        final Method installProvidersMethod = activityThreadClass.getDeclaredMethod(
            "installContentProviders", Context.class, List.class);

        installProvidersMethod.setAccessible(true);
        installProvidersMethod.invoke(currentActivityThread, context, providerInfos);
    }

}


BaseDexClassLoaderHooker

/**
 * DexElements 插桩
 *
 * @author CaMnter
 */

public final class BaseDexClassLoaderHooker {

    @SuppressWarnings("DanglingJavadoc")
    public static void patchClassLoader(@NonNull final ClassLoader classLoader,
                                        @NonNull final File apkFile,
                                        @NonNull final File optDexFile)
        throws IllegalAccessException,
               NoSuchMethodException,
               IOException,
               InvocationTargetException,
               InstantiationException,
               NoSuchFieldException,
               ClassNotFoundException {

        // 获取 BaseDexClassLoader # DexPathList pathList
        final Field pathListField = DexClassLoader.class.getSuperclass()
            .getDeclaredField("pathList");
        pathListField.setAccessible(true);
        final Object pathList = pathListField.get(classLoader);

        // 获取 DexPathList # Element[] dexElements
        final Field dexElementArray = pathList.getClass().getDeclaredField("dexElements");
        dexElementArray.setAccessible(true);
        final Object[] dexElements = (Object[]) dexElementArray.get(pathList);

        // Element 类型
        final Class<?> elementClass = dexElements.getClass().getComponentType();

        // 用于替换 PathList # Element[] dexElements
        final Object[] newElements = (Object[]) Array.newInstance(elementClass,
            dexElements.length + 1);

        /**
         * <= 4.0.0
         *
         * no method
         *
         * >= 4.0.0
         *
         * Element(File file, ZipFile zipFile, DexFile dexFile)
         *
         * ---
         *
         * >= 5.0.0
         *
         * Element(File file, boolean isDirectory, File zip, DexFile dexFile)
         *
         * ---
         *
         * >= 8.0.0
         *
         * @Deprecated
         * Element(File dir, boolean isDirectory, File zip, DexFile dexFile)
         * Element(DexFile dexFile, File dexZipPath)
         *
         */
        final int sdkVersion = Build.VERSION.SDK_INT;

        if (sdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            throw new RuntimeException(
                "[BaseDexClassLoaderHooker]   the sdk version must >= 14 (4.0.0)");
        }

        final Object element;
        final Constructor<?> constructor;

        if (sdkVersion >= Build.VERSION_CODES.O) {
            // >= 8.0.0
            // DexFile dexFile, File dexZipPath
            constructor = elementClass.getConstructor(
                DexFile.class,
                File.class
            );
            element = constructor.newInstance(
                DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0),
                apkFile
            );
        } else if (sdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
            // >= 5.0.0
            // File file, boolean isDirectory, File zip, DexFile dexFile
            constructor = elementClass.getConstructor(
                File.class,
                boolean.class,
                File.class,
                DexFile.class
            );
            element = constructor.newInstance(
                apkFile,
                false,
                apkFile,
                DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)
            );
        } else {
            // >= 4.0.0
            // File file, ZipFile zipFile, DexFile dexFile
            constructor = elementClass.getConstructor(
                File.class,
                ZipFile.class,
                DexFile.class
            );
            element = constructor.newInstance(
                apkFile,
                new ZipFile(apkFile),
                DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)
            );
        }

        final Object[] toAddElementArray = new Object[] { element };
        // 把原始的 elements 复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 把插件的 element  复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length,
            toAddElementArray.length);

        // 替换
        dexElementArray.set(pathList, newElements);
    }

}




访问插件 ContentProvider


ContentProviderPluginActivity

/**
 * @author CaMnter
 */

@SuppressWarnings("DanglingJavadoc")
public class ContentProviderPluginActivity extends BaseAppCompatActivity  
    implements View.OnClickListener {

    private int count = 0;
    private Uri pluginUri;
    private ContentResolver resolver;


    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }


    @Override
    protected void initViews(Bundle savedInstanceState) {
        this.findView(R.id.query_plugin_content_provider).setOnClickListener(this);
        this.findView(R.id.insert_plugin_content_provider).setOnClickListener(this);
        this.findView(R.id.delete_plugin_content_provider).setOnClickListener(this);

        /**
         * 插件 uri
         * content://com.camnter.content.provider.plugin.plugin.PluginContentProvider
         *
         * 实际 uri 得写成
         * content://com.camnter.content.provider.plugin.host.StubContentProvider/com.camnter.content.provider.plugin.plugin.PluginContentProvider
         */
        this.pluginUri = Uri.parse(StubContentProvider.STUB_URI.toString() + '/' +
            "com.camnter.content.provider.plugin.plugin.PluginContentProvider");
        this.resolver = this.getContentResolver();
    }


    @Override
    protected void initListeners() {

    }


    @Override
    protected void initData() {

    }


    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.query_plugin_content_provider:
                final Cursor result = this.resolver.query(this.pluginUri, null, null, null, null);
                if (result == null) return;
                final StringBuilder stringBuilder = new StringBuilder();
                for (result.moveToFirst(); !result.isAfterLast(); result.moveToNext()) {
                    final int id = result.getInt(result.getColumnIndex("_id"));
                    final String content = result.getString(result.getColumnIndex("content"));
                    stringBuilder.append("_id = ")
                        .append(id)
                        .append(", content = ")
                        .append(content)
                        .append(";");
                }
                result.close();
                if (stringBuilder.length() > 0) {
                    final String info = stringBuilder.substring(0, stringBuilder.length() - 1);
                    ToastUtils.show(this, info, Toast.LENGTH_LONG);
                }
                break;
            case R.id.insert_plugin_content_provider:
                final ContentValues values = new ContentValues();
                values.put("content", "Save you from anything - " + ++this.count);
                this.resolver.insert(this.pluginUri, values);
                ToastUtils.show(this, "insert successfully", Toast.LENGTH_LONG);
                break;
            case R.id.delete_plugin_content_provider:
                this.resolver.delete(this.pluginUri, null, null);
                ToastUtils.show(this, "delete successfully", Toast.LENGTH_LONG);
                break;
        }
    }

}




参考资料


Android插件化原理解析——ContentProvider的插件化