package me.ele.amigo; import android.app.Application; import android.app.Instrumentation; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import java.io.File; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.util.Map; import me.ele.amigo.exceptions.LoadPatchApkException; import me.ele.amigo.hook.HookFactory; import me.ele.amigo.reflect.FieldUtils; import me.ele.amigo.release.ApkReleaseActivity; import me.ele.amigo.utils.CommonUtils; import me.ele.amigo.utils.ProcessUtils; import me.ele.amigo.utils.component.ActivityFinder; import me.ele.amigo.utils.component.ContentProviderFinder; import me.ele.amigo.utils.component.ReceiverFinder; import static android.content.pm.PackageManager.GET_META_DATA; import static me.ele.amigo.compat.ActivityThreadCompat.instance; import static me.ele.amigo.reflect.FieldUtils.readField; import static me.ele.amigo.reflect.FieldUtils.readStaticField; import static me.ele.amigo.reflect.FieldUtils.writeField; import static me.ele.amigo.reflect.MethodUtils.invokeMethod; public class Amigo extends Application { private static final String TAG = Amigo.class.getSimpleName(); private static final int SLEEP_DURATION = 200; private static LoadPatchError loadPatchError; private int revertBitFlag = 0; private Application realApplication; private Instrumentation originalInstrumentation = null; private Object originalCallback = null; private boolean shouldHookAmAndPm; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); attachApplication(); } @Override public void onCreate() { super.onCreate(); try { setAPKApplication(realApplication); } catch (Exception e) { // should not happen, if it does happen, we just let it die throw new RuntimeException(e); } if(shouldHookAmAndPm) { try { installAndHook(); } catch (Exception e) { try { clear(this); attachOriginalApplication(); } catch (Exception e1) { throw new RuntimeException(e1); } } } realApplication.onCreate(); } public void attachApplication() { try { String workingChecksum = PatchInfoUtil.getWorkingChecksum(this); Log.e(TAG, "#attachApplication: working checksum = " + workingChecksum); if (TextUtils.isEmpty(workingChecksum) || !PatchApks.getInstance(this).exists(workingChecksum)) { Log.d(TAG, "#attachApplication: Patch apk doesn't exists"); PatchCleaner.clearPatchIfInMainProcess(this); attachOriginalApplication(); return; } if (PatchChecker.checkUpgrade(this)) { Log.d(TAG, "#attachApplication: Host app has upgrade"); PatchCleaner.clearPatchIfInMainProcess(this); attachOriginalApplication(); return; } // ensure load dex process always run host apk not patch apk if (ProcessUtils.isLoadDexProcess(this)) { Log.e(TAG, "#attachApplication: load dex process"); attachOriginalApplication(); return; } if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(workingChecksum)) { Log.e(TAG, "#attachApplication: None main process and patch apk is not released yet"); attachOriginalApplication(); return; } // only release loaded apk in the main process attachPatchApk(workingChecksum); } catch (LoadPatchApkException e) { e.printStackTrace(); loadPatchError = LoadPatchError.record(LoadPatchError.LOAD_ERR, e); //if patch apk fails to run, Amigo will clear working dir with app's next startup clear(this); try { attachOriginalApplication(); } catch (Throwable e2) { throw new RuntimeException(e2); } } catch (Throwable e) { throw new RuntimeException(e); } } private void attachPatchApk(String checksum) throws LoadPatchApkException { try { if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) { PatchInfoUtil.updateDexFileOptStatus(this, checksum, false); releasePatchApk(checksum); } else { PatchChecker.checkDexAndSo(this, checksum); } setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum)); setApkResource(checksum); revertBitFlag |= getClassLoader() instanceof AmigoClassLoader ? 1 : 0; attachPatchedApplication(checksum); PatchCleaner.clearOldPatches(this, checksum); shouldHookAmAndPm = true; Log.i(TAG, "#attachPatchApk: success"); } catch (Exception e) { throw new LoadPatchApkException(e); } } private void installAndHook() throws Exception { boolean gotNewActivity = ActivityFinder.newActivityExistsInPatch(this); if (gotNewActivity) { setApkInstrumentation(); revertBitFlag |= 1 << 1; setApkHandlerCallback(); revertBitFlag |= 1 << 2; } else { Log.d(TAG, "installAndHook: there is no any new activity, skip hooking " + "instrumentation & mH's callback"); } installHookFactory(); dynamicRegisterNewReceivers(); installPatchContentProviders(); } private void setApkResource(String checksum) throws Exception { PatchResourceLoader.loadPatchResources(this, checksum); Log.i(TAG, "hook Resources success"); } private void setApkInstrumentation() throws Exception { Instrumentation oldIns = (Instrumentation) readField(instance(), "mInstrumentation", true); AmigoInstrumentation ins = new AmigoInstrumentation(oldIns); writeField(instance(), "mInstrumentation", ins, true); originalInstrumentation = ins; Log.i(TAG, "hook instrumentation success"); } private void rollbackApkInstrumentation() { try { writeField(instance(), "mInstrumentation", originalInstrumentation, true); } catch (Exception e) { throw new RuntimeException(e); } } private void setApkHandlerCallback() throws Exception { originalCallback = replaceHandlerCallback(this); Log.i(TAG, "hook handler success"); } private static Handler.Callback replaceHandlerCallback(Context context) throws Exception { Handler handler = (Handler) readField(instance(), "mH", true); Object callback = readField(handler, "mCallback", true); if (callback != null && callback.getClass().getName().equals(AmigoCallback.class.getName ())) { return null; } AmigoCallback value = new AmigoCallback(context, (Handler.Callback) callback); writeField(handler, "mCallback", value); return value; } private void dynamicRegisterNewReceivers() { ReceiverFinder.registerNewReceivers(getApplicationContext(), getClassLoader()); Log.i(TAG, "dynamic register new receivers done"); } private void installHookFactory() { HookFactory.install(this, getClassLoader()); Log.i(TAG, "installHookFactory success"); } private void installPatchContentProviders() { ContentProviderFinder.installPatchContentProviders(getApplicationContext()); Log.i(TAG, "installPatchContentProviders done"); } private void rollbackApkHandlerCallback() { try { Handler handler = (Handler) readField(instance(), "mH", true); writeField(handler, "mCallback", originalCallback); } catch (Exception e) { throw new RuntimeException(e); } } private void releasePatchApk(String checksum) throws Exception { //clear previous working dir PatchCleaner.clearWithoutPatchApk(this, checksum); //start a new process to handle time-tense operation ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), GET_META_DATA); String layoutName = null; String themeName = null; if (appInfo.metaData != null) { layoutName = appInfo.metaData.getString("amigo_layout"); themeName = appInfo.metaData.getString("amigo_theme"); } int layoutId = 0; int themeId = 0; if (!TextUtils.isEmpty(layoutName)) { layoutId = getResources().getIdentifier(layoutName, "layout", getPackageName()); } if (!TextUtils.isEmpty(themeName)) { themeId = getResources().getIdentifier(themeName, "style", getPackageName()); } Log.e(TAG, String.format("layoutName-->%s, themeName-->%s", layoutName, themeName)); Log.e(TAG, String.format("layoutId-->%d, themeId-->%d", layoutId, themeId)); releaseDex(checksum, layoutId, themeId); Log.e(TAG, "release apk done"); } private boolean isDexOptDone(String checksum) { return PatchInfoUtil.isDexFileOptimized(this, checksum); } /** * start a new process to release and optimize dex files */ private void waitDexOptDone(String checksum, int layoutId, int themeId) { ApkReleaseActivity.launch(this, checksum, layoutId, themeId); while (!isDexOptDone(checksum)) { try { Thread.sleep(SLEEP_DURATION); } catch (InterruptedException e) { e.printStackTrace(); } } ProcessUtils.startLauncherIntent(this); } private void releaseDex(String checksum, int layoutId, int themeId) { if (!ProcessUtils.isLoadDexProcess(this)) { if (!isDexOptDone(checksum)) { waitDexOptDone(checksum, layoutId, themeId); } } } private boolean isPatchApkFirstRun(String checksum) { return !PatchInfoUtil.getWorkingChecksum(this).equals(checksum); } private void attachOriginalApplication() throws Exception { revertAll(); Class acd = getClassLoader().loadClass("me.ele.amigo.acd"); String applicationName = (String) readStaticField(acd, "n"); realApplication = (Application) getClassLoader().loadClass(applicationName).newInstance(); invokeMethod(realApplication, "attach", getBaseContext()); } private void revertAll() throws Exception { if ((revertBitFlag & 1) != 0) { setAPKClassLoader(Amigo.class.getClassLoader()); } if ((revertBitFlag & (1 << 1)) != 0) { rollbackApkInstrumentation(); } if ((revertBitFlag & (1 << 2)) != 0) { rollbackApkHandlerCallback(); } ReceiverFinder.unregisterNewReceivers(this); HookFactory.uninstallAllHooks(getClassLoader()); PatchResourceLoader.revertLoadPatchResources(); // TODO unregister providers } private void attachPatchedApplication(String patchApkCheckSum) throws Exception { String appName = getPatchApplicationName(patchApkCheckSum); realApplication = (Application) getClassLoader().loadClass(appName).newInstance(); FieldUtils.writeField(getBaseContext(), "mOuterContext", realApplication); invokeMethod(realApplication, "attach", getBaseContext()); } private String getPatchApplicationName(String patchApkCheckSum) throws Exception { String applicationName = null; try { Class acd = getClassLoader().loadClass("me.ele.amigo.acd"); if (acd != null && acd.getClassLoader() == getClassLoader()) { applicationName = (String) readStaticField(acd, "n"); } } catch (ClassNotFoundException classNotFoundExp) { Log.d(TAG, "getPatchApplicationName: " + classNotFoundExp); } // patch apk may not include amigo-lib as a dependency if (applicationName == null) { applicationName = getPackageManager().getPackageArchiveInfo( PatchApks.getInstance(this).patchPath(patchApkCheckSum), 0).applicationInfo .className; } if (applicationName == null) { throw new RuntimeException("can't resolve original application name"); } if(Amigo.class.getName().equals(applicationName)){ // this shouldn't happen, we just throw a exception to avoid // infinite #attachBaseContext recursion throw new RuntimeException("can't resolve original application name"); } return applicationName; } private void setAPKClassLoader(ClassLoader classLoader) throws Exception { writeField(getLoadedApk(), "mClassLoader", classLoader); } private void setAPKApplication(Application application) throws Exception { Object apk = getLoadedApk(); writeField(apk, "mApplication", application); writeField(instance(), "mInitialApplication", application); } private static Object getLoadedApk() throws Exception { @SuppressWarnings("unchecked") Map<String, WeakReference<Object>> mPackages = (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true); for (String s : mPackages.keySet()) { WeakReference wr = mPackages.get(s); if (wr != null && wr.get() != null) { return wr.get(); } } return null; } public static void work(Context context, File patchFile) { work(context, patchFile, true); } public static void workWithoutCheckingSignature(Context context, File patchFile) { work(context, patchFile, false); } private static void work(Context context, File patchFile, boolean checkSignature) { String patchChecksum = PatchChecker.checkPatchAndCopy(context, patchFile, checkSignature); if (checkWithWorkingPatch(context, patchChecksum)) return; if (!PatchInfoUtil.setWorkingChecksum(context, patchChecksum)) return; KillSelfActivity.start(context); } private static boolean checkWithWorkingPatch(Context context, String patchChecksum) { if (Amigo.hasWorked() && PatchInfoUtil.getWorkingChecksum(context).equals(patchChecksum)) { Log.e(TAG, "#checkWithWorking : cannot apply the same patch twice"); return true; } return false; } public static void workLater(Context context, File patchFile) { workLater(context, patchFile, true, null); } public static void workLater(Context context, File patchFile, WorkLaterCallback callback) { workLater(context, patchFile, true, callback); } public static void workLaterWithoutCheckingSignature(Context context, File patchFile) { workLater(context, patchFile, false, null); } public interface WorkLaterCallback { void onPatchApkReleased(boolean success); } private static void workLater(Context context, File patchFile, boolean checkSignature, WorkLaterCallback callback) { String patchChecksum = PatchChecker.checkPatchAndCopy(context, patchFile, checkSignature); if (checkWithWorkingPatch(context, patchChecksum)) return; if (patchChecksum == null) { Log.e(TAG, "#workLater: empty checksum"); return; } if (callback != null) { AmigoService.startReleaseDex(context, patchChecksum, callback); } else { AmigoService.startReleaseDex(context, patchChecksum); } } public static boolean hasWorked() { ClassLoader classLoader = null; try { classLoader = (ClassLoader) readField(getLoadedApk(), "mClassLoader"); } catch (Throwable e) { e.printStackTrace(); } return classLoader != null && classLoader.getClass().getName().equals(AmigoClassLoader.class.getName()); } public static PackageInfo getHostPackageInfo(Context context, int flags) { String hostApkPath = context.getApplicationInfo().sourceDir; return CommonUtils.getPackageInfo(context, new File(hostApkPath), flags); } public static String getWorkingPatchApkChecksum(Context ctx) { if (!hasWorked()) return ""; return PatchInfoUtil.getWorkingChecksum(ctx); } public static void clear(Context context) { PatchInfoUtil.setWorkingChecksum(context, ""); } public static LoadPatchError getLoadPatchError() { return loadPatchError; } /** * this is for some extreme condition, * like some safety app or malicious software replaces Amigo's hook * * @param context * @return */ public static boolean rollAmigoBack(Context context) { return checkAndSetAmigoCallback(context) || checkAndSetAmigoClassLoader(context); } private static boolean checkAndSetAmigoCallback(Context context) { try { //revert current handler callback to null Handler handler = (Handler) readField(instance(), "mH", true); Object callback = readField(handler, "mCallback", true); if (callback != null) { Field[] fields = callback.getClass().getDeclaredFields(); for (Field field : fields) { Object obj = readField(field, callback, true); if (obj == null || !obj.getClass().getName().equals(AmigoCallback.class.getName())) { continue; } writeField(field, callback, null, true); } } return replaceHandlerCallback(context.getApplicationContext()) != null; } catch (Exception e) { //ignore } return false; } private static boolean checkAndSetAmigoClassLoader(Context context) { try { String classloaderName = context.getClassLoader().getClass().getName(); if (classloaderName.equals(AmigoClassLoader.class.getName())) { return false; } Context app = context.getApplicationContext(); ClassLoader classLoader = app.getClass().getClassLoader(); writeField(getLoadedApk(), "mClassLoader", classLoader); return true; } catch (Exception e) { e.printStackTrace(); } return false; } }