/* * Copyright (C) 2014 achellies * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.limemobile.app.plugin.internal; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import com.limemobile.app.plugin.PluginClientActivity; import com.limemobile.app.plugin.PluginClientApplication; import com.limemobile.app.plugin.PluginClientDialogActivity; import com.limemobile.app.plugin.PluginClientDialogFragmentActivity; import com.limemobile.app.plugin.PluginClientFragmentActivity; import com.limemobile.app.plugin.PluginClientService; import com.limemobile.app.plugin.PluginClientTransparentFragmentActivity; import com.limemobile.app.plugin.PluginHostDelegateActivity; import com.limemobile.app.plugin.PluginHostDelegateDialogActivity; import com.limemobile.app.plugin.PluginHostDelegateDialogFragmentActivity; import com.limemobile.app.plugin.PluginHostDelegateFragmentActivity; import com.limemobile.app.plugin.PluginHostDelegateService; import com.limemobile.app.plugin.PluginHostDelegateTransparentActivity; import com.limemobile.app.plugin.PluginHostDelegateTransparentFragmentActivity; import dalvik.system.DexClassLoader; public class PluginClientManager { static final String INTENT_EXTRA_PLUGIN_CLIENT_DEX_PATH = "extra_plugin_dex_path"; static final String INTENT_EXTRA_PLUGIN_CLIENT_ACTIVITY_CLASS = "extra_plugin_activity_class"; static final String INTENT_EXTRA_PLUGIN_CLIENT_SERVICE_CLASS = "extra_plugin_service_class"; static final String INTENT_EXTRA_PLUGIN_CLIENT_CONTENT_PROVIDER_CLASS = "extra_plugin_content_provider_class"; static final String INTENT_EXTRA_PLUGIN_CLIENT_PACKAGE_NAME = "extra_plugin_packagename"; private static volatile PluginClientManager sInstance; private Context mContext; private final Map<String, PluginClientInfo> mPluginClientPackages = Collections .synchronizedMap(new HashMap<String, PluginClientInfo>()); /** * 存储plugin client 的packageName和dex文件的对应关系 */ private final Map<String, String> mPluginClientDexPaths = Collections .synchronizedMap(new HashMap<String, String>()); /** * 因无法控制classLoader的卸载过程,这里需要保存client对应的classLoader,以便在同一次使用时反复卸载和加载, * 导致生成的DexClassLoader不一致的问题 */ private final Map<String, DexClassLoader> mPluginClientDexClassLoaders = Collections .synchronizedMap(new HashMap<String, DexClassLoader>()); /** * 一个Android应用在启动时,首先Dalvik加载的是Android自身的框架。之后会加载APK包中的classes. * dex文件到全局的ClassLoader。最后根据AndroidManifest.xml中指定的类名,创建对应的Activity实例来展示UI。 * * Android通过dalvik.system.DexClassLoader提供了动态加载Java代码的能力,如果我们能够在Activity启动之前 * ,替换全局的ClassLoader(Application.mBase.mPackageInfo.mClassLoader) */ private ClassLoader mPluginHostGlobalClassLoader; private PluginClientManager(Context context) { mContext = context.getApplicationContext(); } public static PluginClientManager sharedInstance(Context context) { if (sInstance == null) { synchronized (PluginClientManager.class) { if (sInstance == null) { sInstance = new PluginClientManager(context); } } } return sInstance; } /** * 设置PluginHost全局的DexClassLoader * * @param classLoader */ public void setPluginHostGlobalClassLoader(ClassLoader classLoader) { this.mPluginHostGlobalClassLoader = classLoader; } /** * add a apk client. Before start a plugin Activity, we should do this * first.<br/> * NOTE : will only be called by host apk. * * @param dexPath */ public PluginClientInfo addPluginClient(String dexPath) { // when loadApk is called by host apk, we assume that plugin is invoked // by host. PackageManager packageManager = mContext.getPackageManager(); int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_CONFIGURATIONS | PackageManager.GET_INSTRUMENTATION | PackageManager.GET_PERMISSIONS | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_SERVICES | PackageManager.GET_SIGNATURES; PackageInfo packageInfo = packageManager.getPackageArchiveInfo(dexPath, flags); if (packageInfo == null) { return null; } final String packageName = packageInfo.packageName; PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { DexClassLoader dexClassLoader = null; if (mPluginClientDexClassLoaders.containsKey(packageName)) { dexClassLoader = mPluginClientDexClassLoaders.get(packageName); } else { dexClassLoader = createDexClassLoader(dexPath, packageInfo.packageName, packageInfo.versionName); mPluginClientDexClassLoaders.put(packageName, dexClassLoader); } AssetManager assetManager = createAssetManager(dexPath); Resources resources = createResources(assetManager); pluginPackage = new PluginClientInfo(packageName, dexPath, dexClassLoader, assetManager, resources, packageInfo); mPluginClientPackages.put(packageName, pluginPackage); } if (!mPluginClientDexPaths.containsKey(packageName)) { mPluginClientDexPaths.put(packageName, dexPath); } return pluginPackage; } public void removePluginClient(String packageName) { PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { return; } if (mContext instanceof Application) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { try { Application.class .getMethod( "unregisterComponentCallbacks", Class.forName("android.content.ComponentCallbacks")) .invoke(mContext, pluginPackage.getApplication()); } catch (Exception e) { e.printStackTrace(); } } } if (pluginPackage.mApplication.getClass().isAssignableFrom( PluginClientApplication.class)) { ((PluginClientApplication) pluginPackage.mApplication).onDestroy(); } mPluginClientPackages.remove(packageName); mPluginClientDexPaths.remove(packageName); pluginPackage = null; } private DexClassLoader createDexClassLoader(String dexPath, String packageName, String version) { File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE); dexOutputDir = new File(dexOutputDir, String.format("/%s/%s/", packageName, version)); dexOutputDir.mkdirs(); final String dexOutputPath = dexOutputDir.getAbsolutePath(); ApplicationInfo ai = mContext.getApplicationInfo(); String nativeLibraryDir = null; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) { nativeLibraryDir = ai.nativeLibraryDir; } else { nativeLibraryDir = "/data/data/" + ai.packageName + "/lib/"; } ClassLoader cl = null; Object object = null; try { /** * 一个Android应用在启动时,首先Dalvik加载的是Android自身的框架。之后会加载APK包中的classes. * dex文件到全局的ClassLoader * 。最后根据AndroidManifest.xml中指定的类名,创建对应的Activity实例来展示UI。 * * Android通过dalvik.system.DexClassLoader提供了动态加载Java代码的能力, * 如果我们能够在Activity启动之前 * ,替换全局的ClassLoader(Application.mBase.mPackageInfo.mClassLoader) * 这里主要是解决多个apk共用一个jar包的问题(例如:多个plugin client共用一个通用的jar) * * http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1223/2206.html * http://www.trinea.cn/android/java-loader-common-class/ * https://github.com/houkx/android-pluginmgr * http://blog.csdn.net/hkxxx/article/details/42194387 * http://blog.csdn.net/czh0766/article/details/6736826 * https://github.com/singwhatiwanna/dynamic-load-apk * http://blog.csdn.net/jiangwei0910410003/article/details/41384667 */ object = ReflectionUtils.getFieldValue(mContext.getApplicationContext(), "mBase.mPackageInfo", true); ReflectFieldAccessor<ClassLoader> fieldAccessor = new ReflectFieldAccessor<ClassLoader>( object, "mClassLoader"); cl = fieldAccessor.get(); } catch (IllegalAccessException e) { } catch (IllegalArgumentException e) { } catch (NoSuchFieldException e) { } finally { if (cl == null) { cl = mContext.getClassLoader(); } } PluginClientDexClassLoader dcl = new PluginClientDexClassLoader( dexPath, dexOutputPath, nativeLibraryDir, cl); // try { // Field f = ClassLoader.class.getDeclaredField("parent"); // f.setAccessible(true); // f.set(cl, dcl); // } catch (Exception e) { // throw new RuntimeException(e); // } return dcl; } private AssetManager createAssetManager(String dexPath) { try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); return assetManager; } catch (Exception e) { e.printStackTrace(); return null; } } private void checkPluginClientApplication(PluginClientInfo pluginPackage) { if (pluginPackage.isApplicationInited()) { return; } String className = pluginPackage.mClientPackageInfo.applicationInfo.className; try { // create Application instance for plugin Application application = null; if (className == null) { application = new Application(); } else { ClassLoader loader = pluginPackage.mClassLoader; Class<?> applicationClass; applicationClass = loader.loadClass(className); application = (Application) applicationClass.newInstance(); } pluginPackage.setApplication(application); // PluginContextWrapper ctxWrapper = new PluginContextWrapper( mContext, pluginPackage); // set field: mBase ReflectionUtils.setFieldValue(application, "mBase", ctxWrapper); // set field: mLoadedApk, get from context(framework application) Object mLoadedApk = ReflectionUtils.getFieldValue(mContext, "mLoadedApk"); ReflectionUtils .setFieldValue(application, "mLoadedApk", mLoadedApk); if (mContext instanceof Application) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Application.class .getMethod( "registerComponentCallbacks", Class.forName("android.content.ComponentCallbacks")) .invoke(mContext, application); } } // invoke plugin application's onCreate() application.onCreate(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } public PluginClientInfo getPluginClient(String packageName) { PluginClientInfo pluginClient = mPluginClientPackages.get(packageName); if (pluginClient == null && mPluginClientDexPaths.containsKey(packageName)) { return addPluginClient(mPluginClientDexPaths.get(packageName)); } return pluginClient; } public Collection<PluginClientInfo> getPluginClients() { return mPluginClientPackages.values(); } private Resources createResources(AssetManager assetManager) { Resources superRes = mContext.getResources(); Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration()); return resources; } public ComponentName startService(Context context, Intent service) { String packageName = service.getPackage(); if (TextUtils.isEmpty(packageName)) { throw new NullPointerException("disallow null packageName."); } if (service.getComponent() == null) { throw new NullPointerException("disallow null component"); } String className = service.getComponent().getClassName(); return startPluginClientService(context, service, packageName, className); } public boolean stopService(Context context, Intent service) { String packageName = service.getPackage(); if (TextUtils.isEmpty(packageName)) { throw new NullPointerException("disallow null packageName."); } if (service.getComponent() == null) { throw new NullPointerException("disallow null component"); } String className = service.getComponent().getClassName(); return stopPluginClientService(context, service, packageName, className); } public boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) { String packageName = service.getPackage(); if (TextUtils.isEmpty(packageName)) { throw new NullPointerException("disallow null packageName."); } if (service.getComponent() == null) { throw new NullPointerException("disallow null component"); } String className = service.getComponent().getClassName(); return bindPluginClientService(context, service, packageName, className, conn, flags); } public ComponentName startPluginClientService(Context context, String packageName, String className) { return startPluginClientService(context, new Intent(), packageName, className); } public ComponentName startPluginClientService(Context context, Intent service, String packageName, String className) { PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { return null; } checkPluginClientApplication(pluginPackage); DexClassLoader classLoader = pluginPackage.mClassLoader; className = (className == null ? pluginPackage.getDefaultActivity() : className); if (className.startsWith(".")) { className = packageName + className; } Class<?> clazz = null; if (mPluginHostGlobalClassLoader != null) { try { clazz = mPluginHostGlobalClassLoader.loadClass(className); } catch (Exception e) { } } if (clazz == null) { try { clazz = classLoader.loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return null; } } Class<? extends Service> serviceClass = null; if (PluginClientService.class.isAssignableFrom(clazz)) { serviceClass = PluginHostDelegateService.class; } else { return null; } service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_SERVICE_CLASS, className); service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_PACKAGE_NAME, packageName); service.setClass(context, serviceClass); return context.startService(service); } public boolean bindPluginClientService(Context context, String packageName, String className, ServiceConnection conn, int flags) { return bindPluginClientService(context, new Intent(), packageName, className, conn, flags); } public boolean bindPluginClientService(Context context, Intent service, String packageName, String className, ServiceConnection conn, int flags) { PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { return false; } checkPluginClientApplication(pluginPackage); DexClassLoader classLoader = pluginPackage.mClassLoader; className = (className == null ? pluginPackage.getDefaultActivity() : className); if (className.startsWith(".")) { className = packageName + className; } Class<?> clazz = null; if (mPluginHostGlobalClassLoader != null) { try { clazz = mPluginHostGlobalClassLoader.loadClass(className); } catch (Exception e) { } } if (clazz == null) { try { clazz = classLoader.loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } } Class<? extends Service> serviceClass = null; if (PluginClientService.class.isAssignableFrom(clazz)) { serviceClass = PluginHostDelegateService.class; } else { return false; } service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_SERVICE_CLASS, className); service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_PACKAGE_NAME, packageName); service.setClass(context, serviceClass); return context.bindService(service, conn, flags); } public boolean stopPluginClientService(Context context, String packageName, String className) { return stopPluginClientService(context, new Intent(), packageName, className); } public boolean stopPluginClientService(Context context, Intent service, String packageName, String className) { PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { return false; } checkPluginClientApplication(pluginPackage); DexClassLoader classLoader = pluginPackage.mClassLoader; className = (className == null ? pluginPackage.getDefaultActivity() : className); if (className.startsWith(".")) { className = packageName + className; } Class<?> clazz = null; if (mPluginHostGlobalClassLoader != null) { try { clazz = mPluginHostGlobalClassLoader.loadClass(className); } catch (Exception e) { } } if (clazz == null) { try { clazz = classLoader.loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } } Class<? extends Service> serviceClass = null; if (PluginClientService.class.isAssignableFrom(clazz)) { serviceClass = PluginHostDelegateService.class; } else { return false; } service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_SERVICE_CLASS, className); service.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_PACKAGE_NAME, packageName); service.setClass(context, serviceClass); return context.stopService(service); } public void startPluginClientActivity(Context context, String packageName, String className) { startPluginActivityForResult(context, new Intent(), packageName, className, -1, null); } public void startActivityForResult(Context context, Intent intent, int requestCode) { startPluginActivityForResult(context, intent, requestCode, null); } public void startActivityForResult(Context context, Intent intent, int requestCode, Bundle options) { startPluginActivityForResult(context, intent, requestCode, options); } public void startActivity(Context context, Intent intent) { startPluginActivityForResult(context, intent, -1, null); } public void startActivity(Context context, Intent intent, Bundle options) { startPluginActivityForResult(context, intent, -1, options); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void startPluginActivityForResult(Context context, Intent intent, int requestCode, Bundle options) { String packageName = intent.getPackage(); if (TextUtils.isEmpty(packageName)) { throw new NullPointerException("disallow null packageName."); } if (intent.getComponent() == null) { throw new NullPointerException("disallow null component"); } String className = intent.getComponent().getClassName(); startPluginActivityForResult(context, intent, packageName, className, requestCode, options); } private void startPluginActivityForResult(Context context, Intent intent, String packageName, String className, int requestCode, Bundle options) { // TODO 是否不需要传入Intent PluginClientInfo pluginPackage = mPluginClientPackages.get(packageName); if (pluginPackage == null) { return; } checkPluginClientApplication(pluginPackage); DexClassLoader classLoader = pluginPackage.mClassLoader; className = (className == null ? pluginPackage.getDefaultActivity() : className); if (className.startsWith(".")) { className = packageName + className; } Class<?> clazz = null; if (mPluginHostGlobalClassLoader != null) { try { clazz = mPluginHostGlobalClassLoader.loadClass(className); } catch (Exception e) { } } if (clazz == null) { try { clazz = classLoader.loadClass(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return; } } Class<? extends Activity> activityClass = null; if (PluginClientDialogActivity.class.isAssignableFrom(clazz)) { activityClass = PluginHostDelegateDialogActivity.class; } else if (PluginClientDialogFragmentActivity.class .isAssignableFrom(clazz)) { activityClass = PluginHostDelegateTransparentActivity.class; } else if (PluginClientActivity.class.isAssignableFrom(clazz)) { activityClass = PluginHostDelegateActivity.class; } else if (PluginClientDialogFragmentActivity.class .isAssignableFrom(clazz)) { activityClass = PluginHostDelegateDialogFragmentActivity.class; } else if (PluginClientTransparentFragmentActivity.class .isAssignableFrom(clazz)) { activityClass = PluginHostDelegateTransparentFragmentActivity.class; } else if (PluginClientFragmentActivity.class.isAssignableFrom(clazz)) { activityClass = PluginHostDelegateFragmentActivity.class; } else { return; } // TODO 处理options intent.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_ACTIVITY_CLASS, className); intent.putExtra(INTENT_EXTRA_PLUGIN_CLIENT_PACKAGE_NAME, packageName); intent.setClass(context, activityClass); performStartActivityForResult(context, intent, requestCode); } private void performStartActivityForResult(Context context, Intent intent, int requestCode) { if (context instanceof Activity) { ((Activity) context).startActivityForResult(intent, requestCode); } else { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } } public static PackageInfo getPackageInfo(Context context, String apkFilepath) { PackageManager pm = context.getPackageManager(); PackageInfo pkgInfo = null; try { pkgInfo = pm.getPackageArchiveInfo(apkFilepath, PackageManager.GET_ACTIVITIES); } catch (Exception e) { // should be something wrong with parse e.printStackTrace(); } return pkgInfo; } public static Drawable getAppIcon(Context context, String apkFilepath) { PackageManager pm = context.getPackageManager(); PackageInfo pkgInfo = getPackageInfo(context, apkFilepath); if (pkgInfo == null) { return null; } // Workaround for http://code.google.com/p/android/issues/detail?id=9151 ApplicationInfo appInfo = pkgInfo.applicationInfo; if (Build.VERSION.SDK_INT >= 8) { appInfo.sourceDir = apkFilepath; appInfo.publicSourceDir = apkFilepath; } return pm.getApplicationIcon(appInfo); } public static String getAppLabel(Context context, String apkFilepath) { PackageManager pm = context.getPackageManager(); PackageInfo pkgInfo = getPackageInfo(context, apkFilepath); if (pkgInfo == null) { return null; } // Workaround for http://code.google.com/p/android/issues/detail?id=9151 ApplicationInfo appInfo = pkgInfo.applicationInfo; if (Build.VERSION.SDK_INT >= 8) { appInfo.sourceDir = apkFilepath; appInfo.publicSourceDir = apkFilepath; } return pm.getApplicationLabel(appInfo).toString(); } public static int getAppVersionCode(Context context, String apkFilepath) { int version = 0; PackageInfo pkgInfo = getPackageInfo(context, apkFilepath); if (pkgInfo == null) { return version; } version = pkgInfo.versionCode; return version; } public static String getAppVersionName(Context context, String apkFilepath) { String version = ""; PackageInfo pkgInfo = getPackageInfo(context, apkFilepath); if (pkgInfo == null) { return version; } version = pkgInfo.versionName; return version; } }