package com.hanhuy.android.protify.agent; import android.annotation.TargetApi; import android.app.Application; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.os.Build; import android.util.Log; import android.util.Pair; import com.hanhuy.android.protify.agent.internal.*; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Map; /** * some of this is straight up ripped off from bazelbuild's StubApplication * https://github.com/bazelbuild/bazel/blob/3eb0687fde3745cf52bbbb513f7769ecb9d004e4/src/tools/android/java/com/google/devtools/build/android/incrementaldeployment/StubApplication.java * @author pfnguyen */ @SuppressWarnings("unused") public class ProtifyApplication extends Application { private final static String TAG = "ProtifyApplication"; private final String realApplicationClass; private Application realApplication; private final static int NOTIFICATION_ID = 0x70726f74; // = "prot" public ProtifyApplication() { String[] applicationInfo = getResourceAsString("protify_application_info.txt").split("\n"); realApplicationClass = applicationInfo[0].trim(); Log.d(TAG, "Real application class: [" + realApplicationClass + "]"); Protify.installed = true; } @SuppressWarnings("deprecation") private static Notification loadingNotification(Context c, String text) { final Notification n; // R is filtered out of DEX, find the resource manually int icon = c.getResources().getIdentifier( "protify_internal_ic_notification_loading", "drawable", c.getPackageName()); if (icon == 0) throw new IllegalStateException( "protify_internal_ic_notification_loading not found"); if (Build.VERSION.SDK_INT >= 14) { final Notification.Builder nb = new Notification.Builder(c); nb .setContentTitle(text) .setSmallIcon(icon) .setProgress(100, 0, true) .setOngoing(true); n = nb.getNotification(); } else { n = new Notification(); n.icon = icon; n.flags = Notification.FLAG_ONGOING_EVENT; n.tickerText = text; n.setLatestEventInfo(c, text, null, PendingIntent.getBroadcast(c, 0, new Intent( "com.hanhuy.android.protify.internal.action.NOOP"), 0)); } n.when = System.currentTimeMillis(); return n; } private Object stashedContentProviders; private static Field getField(Object instance, String fieldName) throws ClassNotFoundException { for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return field; } catch (NoSuchFieldException e) { // IllegalStateException will be thrown below } } throw new IllegalStateException("Field '" + fieldName + "' not found"); } private void enableContentProviders() { Log.v(TAG, "enableContentProviders"); try { Class<?> activityThread = Class.forName("android.app.ActivityThread"); Method mCurrentActivityThread = activityThread.getMethod("currentActivityThread"); mCurrentActivityThread.setAccessible(true); Object currentActivityThread = mCurrentActivityThread.invoke(null); Object boundApplication = getField( currentActivityThread, "mBoundApplication").get(currentActivityThread); getField(boundApplication, "providers").set(boundApplication, stashedContentProviders); if (stashedContentProviders != null) { Method mInstallContentProviders = activityThread.getDeclaredMethod( "installContentProviders", Context.class, List.class); mInstallContentProviders.setAccessible(true); mInstallContentProviders.invoke( currentActivityThread, realApplication, stashedContentProviders); stashedContentProviders = null; } } catch (Exception e) { if (stashedContentProviders != null) { Log.e(TAG, "ContentProviders stashed, but unable to restore"); throw new IllegalStateException(e); } } } private void disableContentProviders() { Log.v(TAG, "disableContentProviders"); try { Class<?> activityThread = Class.forName("android.app.ActivityThread"); Method mCurrentActivityThread = activityThread.getMethod("currentActivityThread"); mCurrentActivityThread.setAccessible(true); Object currentActivityThread = mCurrentActivityThread.invoke(null); Object boundApplication = getField( currentActivityThread, "mBoundApplication").get(currentActivityThread); Field fProviders = getField(boundApplication, "providers"); stashedContentProviders = fProviders.get(boundApplication); fProviders.set(boundApplication, null); } catch (Exception e) { Log.e(TAG, "Unable to inject Application for ContentProviders"); } } @Override protected void attachBaseContext(Context base) { NotificationManager nm = (NotificationManager) base.getSystemService( NOTIFICATION_SERVICE); CharSequence name; try { name = base.getResources().getText(base.getApplicationInfo().labelRes); } catch (Resources.NotFoundException e) { name = base.getPackageName(); } nm.notify(NOTIFICATION_ID, loadingNotification( base, "Protifying DEX for " + name)); DexLoader.install(base); nm.cancel(NOTIFICATION_ID); createRealApplication(); super.attachBaseContext(base); try { Method attachBaseContext = ContextWrapper.class.getDeclaredMethod( "attachBaseContext", Context.class); attachBaseContext.setAccessible(true); attachBaseContext.invoke(realApplication, base); disableContentProviders(); } catch (Exception e) { throw new IllegalStateException(e); } if (Build.VERSION.SDK_INT >= 14) { realApplication.registerActivityLifecycleCallbacks( LifecycleListener.getInstance()); } } @Override public void onCreate() { installRealApplication(); installExternalResources(this); enableContentProviders(); super.onCreate(); realApplication.onCreate(); } @SuppressWarnings("unchecked") private void installRealApplication() { // StubApplication is created by reflection in Application#handleBindApplication() -> // LoadedApk#makeApplication(), and its return value is used to set the Application field in all // sorts of Android internals. // // Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey // patch in the real Application instance in StubApplication#onCreate(). // // A few places directly use the created Application instance (as opposed to the fields it is // eventually stored in). Fortunately, it's easy to forward those to the actual real // Application class. try { // Find the ActivityThread instance for the current thread Class<?> activityThread = Class.forName("android.app.ActivityThread"); Method m = activityThread.getMethod("currentActivityThread"); m.setAccessible(true); Object currentActivityThread = m.invoke(null); // Find the mInitialApplication field of the ActivityThread to the real application Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication"); mInitialApplication.setAccessible(true); Application initialApplication = (Application) mInitialApplication.get(currentActivityThread); if (initialApplication == this) { mInitialApplication.set(currentActivityThread, realApplication); } // Replace all instance of the stub application in ActivityThread#mAllApplications with the // real one Field mAllApplications = activityThread.getDeclaredField("mAllApplications"); mAllApplications.setAccessible(true); List<Application> allApplications = (List<Application>) mAllApplications .get(currentActivityThread); for (int i = 0; i < allApplications.size(); i++) { if (allApplications.get(i) == this) { allApplications.set(i, realApplication); } } // Figure out how loaded APKs are stored. // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. Class<?> loadedApkClass; try { loadedApkClass = Class.forName("android.app.LoadedApk"); } catch (ClassNotFoundException e) { loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo"); } Field mApplication = loadedApkClass.getDeclaredField("mApplication"); mApplication.setAccessible(true); Field mResDir = loadedApkClass.getDeclaredField("mResDir"); mResDir.setAccessible(true); // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices // floating around. Field mLoadedApk = null; try { mLoadedApk = Application.class.getDeclaredField("mLoadedApk"); } catch (NoSuchFieldException e) { // According to testing, it's okay to ignore this. } // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and // ActivityThread#mResourcePackages and do two things: // - Replace the Application instance in its mApplication field with the real one // - Replace mResDir to point to the external resource file instead of the .apk. This is // used as the asset path for new Resources objects. // - Set Application#mLoadedApk to the found LoadedApk instance for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) { Field field = activityThread.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value).entrySet()) { Object loadedApk = entry.getValue().get(); if (loadedApk == null) { continue; } if (mApplication.get(loadedApk) == this) { File externalResourceFile = ProtifyResources.getResourcesFile(this); mApplication.set(loadedApk, realApplication); ApplicationInfo info = getApplicationInfo(); if (info != null && new File(info.sourceDir).lastModified() > externalResourceFile.lastModified()) { Log.v(TAG, "Deleting out of date external resources"); externalResourceFile.delete(); } if (externalResourceFile.isFile()) { Log.v(TAG, "Setting mResDir to: " + externalResourceFile.getAbsolutePath()); mResDir.set(loadedApk, externalResourceFile.getAbsolutePath()); } if (mLoadedApk != null) { mLoadedApk.set(realApplication, loadedApk); } } } } } catch (Exception e) { throw new IllegalStateException(e); } } public static void installExternalResources(Context context) { File f = ProtifyResources.getResourcesFile(context); ApplicationInfo info = context.getApplicationInfo(); if (info != null && new File(info.sourceDir).lastModified() > f.lastModified()) { Log.v(TAG, "Deleting outdated external resources"); f.delete(); } if (f.isFile() && f.length() > 0) { Log.v(TAG, "Installing external resource file: " + f); if (Build.VERSION.SDK_INT >= 24) V24Resources.install(f.getAbsolutePath()); else if (Build.VERSION.SDK_INT >= 18) V19Resources.install(f.getAbsolutePath()); else V4Resources.install(f.getAbsolutePath()); resourceInstallTime = System.currentTimeMillis(); } } private static long resourceInstallTime = System.currentTimeMillis(); public static long getResourceInstallTime() { return resourceInstallTime; } private String getResourceAsString(String resource) { InputStream resourceStream = null; // try-with-resources would be much nicer, but that requires SDK level 19, and we want this code // to be compatible with earlier Android versions try { resourceStream = getClass().getClassLoader().getResourceAsStream(resource); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = resourceStream.read(buffer)) != -1) { baos.write(buffer, 0, length); } return new String(baos.toByteArray(), "UTF-8"); } catch (IOException e) { throw new IllegalStateException(e); } finally { if (resourceStream != null) { try { resourceStream.close(); } catch (IOException e) { // Not much we can do here } } } } private void createRealApplication() { try { @SuppressWarnings("unchecked") Class<? extends Application> realClass = (Class<? extends Application>) Class.forName(realApplicationClass); Constructor<? extends Application> ctor = realClass.getConstructor(); realApplication = ctor.newInstance(); } catch (Exception e) { throw new IllegalStateException(e); } } private static class V24Resources { @TargetApi(24) static void install(String externalResourceFile) { try { AssetManager newAssetManager = createAssetManager(externalResourceFile); // Find the singleton instance of ResourcesManager Class<?> clazz = Class.forName("android.app.ResourcesManager"); Method mGetInstance = clazz.getDeclaredMethod("getInstance"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); // Iterate over all known Resources objects Field mResourceReferences = clazz.getDeclaredField("mResourceReferences"); mResourceReferences.setAccessible(true); @SuppressWarnings("unchecked") Collection<WeakReference<Resources>> references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager); setAssetManager(references, newAssetManager); } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException | InstantiationException e) { throw new IllegalStateException(e); } } } private static class V19Resources { @TargetApi(19) static void install(String externalResourceFile) { try { AssetManager newAssetManager = createAssetManager(externalResourceFile); // Find the singleton instance of ResourcesManager Class<?> clazz = Class.forName("android.app.ResourcesManager"); Method mGetInstance = clazz.getDeclaredMethod("getInstance"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); // Iterate over all known Resources objects Field fMActiveResources = clazz.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); @SuppressWarnings("unchecked") Map<?, WeakReference<Resources>> arrayMap = (Map<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager); setAssetManager(arrayMap, newAssetManager); } catch (IllegalAccessException | NoSuchFieldException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException | InstantiationException e) { throw new IllegalStateException(e); } } } private static class V4Resources { static void install(String externalResourceFile) { try { AssetManager newAssetManager = createAssetManager(externalResourceFile); // Find the singleton instance of ResourcesManager Class<?> clazz = Class.forName("android.app.ActivityThread"); Method mGetInstance = clazz.getDeclaredMethod("currentActivityThread"); mGetInstance.setAccessible(true); Object resourcesManager = mGetInstance.invoke(null); // Iterate over all known Resources objects Field fMActiveResources = clazz.getDeclaredField("mActiveResources"); fMActiveResources.setAccessible(true); @SuppressWarnings("unchecked") Map<?, WeakReference<Resources>> arrayMap = (Map<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager); setAssetManager(arrayMap, newAssetManager); } catch (Exception e) { throw new IllegalStateException(e); } } } private static AssetManager createAssetManager(String externalResourceFile) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { // Create a new AssetManager instance and point it to the resources installed under // /sdcard AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance(); Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); mAddAssetPath.setAccessible(true); if (((int) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) { throw new IllegalStateException("Could not create new AssetManager"); } // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm // in L, so we do it unconditionally. Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks"); mEnsureStringBlocks.setAccessible(true); mEnsureStringBlocks.invoke(newAssetManager); return newAssetManager; } private static void setAssetManager(Map<?,WeakReference<Resources>> arrayMap, AssetManager newAssetManager) throws IllegalAccessException, NoSuchFieldException { setAssetManager(arrayMap.values(), newAssetManager); } private static void setAssetManager(Collection<WeakReference<Resources>> ress, AssetManager newAssetManager) throws IllegalAccessException, NoSuchFieldException { for (WeakReference<Resources> wr : ress) { Resources resources = wr.get(); // Set the AssetManager of the Resources instance to our brand new one if (resources != null) { setAssetsField(resources, newAssetManager); resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics()); } } } private static void setAssetsField(Resources resources, AssetManager newAssetManager) { Field mAssets; try { if (Build.VERSION.SDK_INT >= 24) { Field mResourcesImplField; mResourcesImplField = Resources.class.getDeclaredField("mResourcesImpl"); mResourcesImplField.setAccessible(true); Object mResourceImpl = mResourcesImplField.get(resources); mAssets = mResourceImpl.getClass().getDeclaredField("mAssets"); mAssets.setAccessible(true); mAssets.set(mResourceImpl, newAssetManager); } else { mAssets = Resources.class.getDeclaredField("mAssets"); mAssets.setAccessible(true); mAssets.set(resources, newAssetManager); } } catch (Exception e) { throw new IllegalStateException(e); } } }