/*
* Copyright (C) 2015 HouKx <hkx.aidream@gmail.com>
*
* 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 androidx.pluginmgr;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;
/**
* 插件管理器
*
* @author HouKangxi
*/
public class PluginManager implements FileFilter {
private static final PluginManager instance = new PluginManager();
private Activity actFrom;
private PluginManager() {
}
public static PluginManager getInstance(Context context) {
if (instance.hasInit || context == null) {
return instance;
}
Context ctx = context;
if (context instanceof Activity) {
instance.actFrom = (Activity) context;
ctx = ((Activity) context).getApplication();
} else if (context instanceof Service) {
ctx = ((Service) context).getApplication();
} else if (context instanceof Application) {
ctx = (Application) context;
} else {
ctx = context.getApplicationContext();
}
synchronized (PluginManager.class) {
instance.init(ctx);
}
return instance;
}
static PluginManager getInstance() {
return instance;
}
public boolean startMainActivity(Context context, String pkgOrId) {
Log.d(tag, "startMainActivity by:" + pkgOrId);
PlugInfo plug = preparePlugForStartActivity(context, pkgOrId);
if (frameworkClassLoader == null) {
Log.e(tag, "startMainActivity: frameworkClassLoader == null!");
return false;
}
if (plug.getMainActivity() == null) {
Log.e(tag, "startMainActivity: plug.getMainActivity() == null!");
return false;
}
if (plug.getMainActivity().activityInfo == null) {
Log.e(tag,
"startMainActivity: plug.getMainActivity().activityInfo == null!");
return false;
}
String className = frameworkClassLoader.newActivityClassName(
plug.getId(), plug.getMainActivity().activityInfo.name);
context.startActivity(new Intent().setComponent(new ComponentName(
context, className)));
return true;
}
public void startActivity(Context context, Intent intent) {
performStartActivity(context, intent);
context.startActivity(intent);
}
public void startActivityForResult(Activity activity, Intent intent,
int requestCode) {
performStartActivity(context, intent);
activity.startActivityForResult(intent, requestCode);
}
private PlugInfo preparePlugForStartActivity(Context context,
String plugIdOrPkg) {
PlugInfo plug = null;
plug = getPluginByPackageName(plugIdOrPkg);
if (plug == null) {
plug = getPluginById(plugIdOrPkg);
}
if (plug == null) {
throw new IllegalArgumentException("plug not found by:"
+ plugIdOrPkg);
}
return plug;
}
private void performStartActivity(Context context, Intent intent) {
checkInit();
String plugIdOrPkg;
String actName;
ComponentName origComp = intent.getComponent();
if (origComp != null) {
plugIdOrPkg = origComp.getPackageName();
actName = origComp.getClassName();
} else {
throw new IllegalArgumentException(
"plug intent must set the ComponentName!");
}
PlugInfo plug = preparePlugForStartActivity(context, plugIdOrPkg);
String className = frameworkClassLoader.newActivityClassName(
plug.getId(), actName);
Log.i(tag, "performStartActivity: " + actName);
ComponentName comp = new ComponentName(context, className);
intent.setAction(null);
intent.setComponent(comp);
}
private final Map<String, PlugInfo> pluginIdToInfoMap = new ConcurrentHashMap<String, PlugInfo>();
private final Map<String, PlugInfo> pluginPkgToInfoMap = new ConcurrentHashMap<String, PlugInfo>();
private Context context;
private String dexOutputPath;
private volatile boolean hasInit = false;
private File dexInternalStoragePath;
private FrameworkClassLoader frameworkClassLoader;
private PluginActivityLifeCycleCallback pluginActivityLifeCycleCallback;
private static final String tag = "plugmgr";
private void init(Context ctx) {
context = ctx;
File optimizedDexPath = ctx.getDir("plugsout", Context.MODE_PRIVATE);
if (!optimizedDexPath.exists()) {
optimizedDexPath.mkdirs();
}
dexOutputPath = optimizedDexPath.getAbsolutePath();
dexInternalStoragePath = context
.getDir("plugins", Context.MODE_PRIVATE);
dexInternalStoragePath.mkdirs();
try {
Object mPackageInfo = ReflectionUtils.getFieldValue(ctx,
"mBase.mPackageInfo", true);
frameworkClassLoader = new FrameworkClassLoader(
ctx.getClassLoader());
// set Application's classLoader to FrameworkClassLoader
ReflectionUtils.setFieldValue(mPackageInfo, "mClassLoader",
frameworkClassLoader, true);
} catch (Exception e) {
e.printStackTrace();
}
hasInit = true;
}
private void checkInit() {
if (!hasInit) {
throw new IllegalStateException("PluginManager has not init!");
}
}
public PlugInfo getPluginById(String pluginId) {
if (pluginId == null) {
return null;
}
return pluginIdToInfoMap.get(pluginId);
}
public PlugInfo getPluginByPackageName(String packageName) {
return pluginPkgToInfoMap.get(packageName);
}
public Collection<PlugInfo> getPlugins() {
return pluginIdToInfoMap.values();
}
public void uninstallPluginById(String pluginId) {
uninstallPlugin(pluginId, true);
}
public void uninstallPluginByPkg(String pkg) {
uninstallPlugin(pkg, false);
}
private void uninstallPlugin(String k, boolean isId) {
checkInit();
PlugInfo pl = isId ? removePlugById(k) : removePlugByPkg(k);
if (pl == null) {
return;
}
if (context instanceof Application) {
if (android.os.Build.VERSION.SDK_INT >= 14) {
try {
Application.class
.getMethod(
"unregisterComponentCallbacks",
Class.forName("android.content.ComponentCallbacks"))
.invoke(context, pl.getApplication());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private PlugInfo removePlugById(String pluginId) {
PlugInfo pl = null;
synchronized (this) {
pl = pluginIdToInfoMap.remove(pluginId);
if (pl == null) {
return null;
}
pluginPkgToInfoMap.remove(pl.getPackageName());
}
return pl;
}
private PlugInfo removePlugByPkg(String pkg) {
PlugInfo pl = null;
synchronized (this) {
pl = pluginPkgToInfoMap.remove(pkg);
if (pl == null) {
return null;
}
pluginIdToInfoMap.remove(pl.getId());
}
return pl;
}
/**
* 加载指定插件或指定目录下的所有插件
* <p>
* 都使用文件名作为Id
*
* @param pluginSrcDirFile
* - apk或apk目录
* @return 插件集合
* @throws Exception
*/
public Collection<PlugInfo> loadPlugin(final File pluginSrcDirFile)
throws Exception {
checkInit();
if (pluginSrcDirFile == null || !pluginSrcDirFile.exists()) {
Log.e(tag, "invalidate plugin file or Directory :"
+ pluginSrcDirFile);
return null;
}
if (pluginSrcDirFile.isFile()) {
// 如果是文件则尝试加载单个插件,暂不检查文件类型,除apk外,以后可能支持加载其他类型文件,如jar
PlugInfo one = loadPluginWithId(pluginSrcDirFile, null, null);
return Collections.singletonList(one);
}
// clear all first
synchronized (this) {
pluginPkgToInfoMap.clear();
pluginIdToInfoMap.clear();
}
File[] pluginApks = pluginSrcDirFile.listFiles(this);
if (pluginApks == null || pluginApks.length < 1) {
throw new FileNotFoundException("could not find plugins in:"
+ pluginSrcDirFile);
}
for (File pluginApk : pluginApks) {
PlugInfo plugInfo = buildPlugInfo(pluginApk, null, null);
if (plugInfo != null) {
savePluginToMap(plugInfo);
}
}
return pluginIdToInfoMap.values();
}
private synchronized void savePluginToMap(PlugInfo plugInfo) {
pluginPkgToInfoMap.put(plugInfo.getPackageName(), plugInfo);
pluginIdToInfoMap.put(plugInfo.getId(), plugInfo);
}
// /**
// * 单独加载一个apk <br/>
// * 使用文件名作为插件id <br/>
// * 目标文件也是与源文件同名
// *
// * @param pluginApk
// * @return
// * @throws Exception
// */
// public PlugInfo loadPlugin(File pluginApk) throws Exception {
// return loadPluginWithId(pluginApk, null, null);
// }
/**
* 单独加载一个apk
*
* @param pluginApk
* @param pluginId
* - 如果参数为null,则使用文件名作为插件id
* @return
* @throws Exception
*/
public PlugInfo loadPluginWithId(File pluginApk, String pluginId)
throws Exception {
return loadPluginWithId(pluginApk, pluginId, null);
}
public PlugInfo loadPluginWithId(File pluginApk, String pluginId,
String targetFileName) throws Exception {
checkInit();
PlugInfo plugInfo = buildPlugInfo(pluginApk, pluginId, targetFileName);
if (plugInfo != null) {
savePluginToMap(plugInfo);
}
return plugInfo;
}
private PlugInfo buildPlugInfo(File pluginApk, String pluginId,
String targetFileName) throws Exception {
PlugInfo info = new PlugInfo();
info.setId(pluginId == null ? pluginApk.getName() : pluginId);
File privateFile = new File(dexInternalStoragePath,
targetFileName == null ? pluginApk.getName() : targetFileName);
info.setFilePath(privateFile.getAbsolutePath());
if (!pluginApk.getAbsolutePath().equals(privateFile.getAbsolutePath())) {
copyApkToPrivatePath(pluginApk, privateFile);
}
String dexPath = privateFile.getAbsolutePath();
PluginManifestUtil.setManifestInfo(context, dexPath, info);
PluginClassLoader loader = new PluginClassLoader(dexPath,
dexOutputPath, frameworkClassLoader, info);
info.setClassLoader(loader);
try {
AssetManager am = (AssetManager) AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
.invoke(am, dexPath);
info.setAssetManager(am);
Resources ctxres = context.getResources();
Resources res = new Resources(am, ctxres.getDisplayMetrics(),
ctxres.getConfiguration());
info.setResources(res);
} catch (Exception e) {
e.printStackTrace();
}
initPluginApplication(info);
// createPluginActivityProxyDexes(info);
Log.i(tag, "buildPlugInfo: " + info);
return info;
}
// private void createPluginActivityProxyDexes(PlugInfo plugin) {
// {
// ActInfo act = plugin.getApplicationInfo().getMainActivity();
// if (act != null) {
// ActivityOverider.createProxyDex(plugin, act.name, false);
// }
// }
// if (plugin.getApplicationInfo().getOtherActivities() != null) {
// for (ActInfo act : plugin.getApplicationInfo().getOtherActivities())
// {
// ActivityOverider.createProxyDex(plugin, act.name, false);
// }
// }
// }
private void initPluginApplication(final PlugInfo info) throws Exception {
String className = info.getPackageInfo().applicationInfo.name;
Log.d(tag, info.getId() + ", ApplicationClassName = " + className);
// create Application instance for plugin
if (className == null) {
Application application = new Application();
setApplicationBase(info, application);
} else {
ClassLoader loader = info.getClassLoader();
final Class<?> applicationClass = loader.loadClass(className);
if (actFrom != null) {
actFrom.runOnUiThread(new Runnable() {
public void run() {
try {
Application application = (Application) applicationClass
.newInstance();
setApplicationBase(info, application);
// invoke plugin application's onCreate()
application.onCreate();
} catch (Throwable e) {
Log.e(tag, Log.getStackTraceString(e));
}
}
});
} else {
Application application = (Application) applicationClass
.newInstance();
setApplicationBase(info, application);
}
}
}
private void setApplicationBase(PlugInfo info, Application application)
throws Exception {
info.setApplication(application);
//
PluginContextWrapper ctxWrapper = new PluginContextWrapper(context,
info);
// attach
java.lang.reflect.Method attachMethod = android.app.Application.class
.getDeclaredMethod("attach", Context.class);
attachMethod.setAccessible(true);
attachMethod.invoke(application, ctxWrapper);
if (context instanceof Application) {
if (android.os.Build.VERSION.SDK_INT >= 14) {
Application.class.getMethod("registerComponentCallbacks",
Class.forName("android.content.ComponentCallbacks"))
.invoke(context, application);
}
}
}
private void copyApkToPrivatePath(File pluginApk, File f) {
// if (f.exists() && pluginApk.length() == f.length()) {
// // 这里只是简单的判断如果两个文件长度相同则不拷贝,严格的做应该比较签名如 md5\sha-1
// return;
// }
FileUtil.copyFile(pluginApk, f);
}
File getDexInternalStoragePath() {
return dexInternalStoragePath;
}
Context getContext() {
return context;
}
public PluginActivityLifeCycleCallback getPluginActivityLifeCycleCallback() {
return pluginActivityLifeCycleCallback;
}
public void setPluginActivityLifeCycleCallback(
PluginActivityLifeCycleCallback pluginActivityLifeCycleCallback) {
this.pluginActivityLifeCycleCallback = pluginActivityLifeCycleCallback;
}
FrameworkClassLoader getFrameworkClassLoader() {
return frameworkClassLoader;
}
@Override
public boolean accept(File pathname) {
if (pathname.isDirectory()) {
return false;
}
String fname = pathname.getName();
return fname.endsWith(".apk");
}
}