/* * Copyright (C) 2017 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved. * * 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.didi.virtualapk.internal; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityThread; import android.app.LoadedApk; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.ResourcesKey; import android.os.Build; import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Log; import com.didi.virtualapk.PluginManager; import com.didi.virtualapk.utils.Reflector; import java.io.File; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** * Created by renyugang on 16/8/9. */ class ResourcesManager { public static final String TAG = "LoadedPlugin"; private static Configuration mDefaultConfiguration; public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return createResourcesForN(hostContext, packageName, apk); } Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath()); ResourcesManager.hookResources(hostContext, resources); return resources; } private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception { Resources hostResources = hostContext.getResources(); Resources newResources = null; AssetManager assetManager; Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { assetManager = AssetManager.class.newInstance(); reflector.bind(assetManager); final int cookie1 = reflector.call(hostContext.getApplicationInfo().sourceDir);; if (cookie1 == 0) { throw new RuntimeException("createResources failed, can't addAssetPath for " + hostContext.getApplicationInfo().sourceDir); } } else { assetManager = hostResources.getAssets(); reflector.bind(assetManager); } final int cookie2 = reflector.call(apk); if (cookie2 == 0) { throw new RuntimeException("createResources failed, can't addAssetPath for " + apk); } List pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins(); for (LoadedPlugin plugin : pluginList) { final int cookie3 = reflector.call(plugin.getLocation()); if (cookie3 == 0) { throw new RuntimeException("createResources failed, can't addAssetPath for " + plugin.getLocation()); } } if (isMiUi(hostResources)) { newResources = MiUiResourcesCompat.createResources(hostResources, assetManager); } else if (isVivo(hostResources)) { newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager); } else if (isNubia(hostResources)) { newResources = NubiaResourcesCompat.createResources(hostResources, assetManager); } else if (isNotRawResources(hostResources)) { newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager); } else { // is raw android resources newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } // lastly, sync all LoadedPlugin to newResources for (LoadedPlugin plugin : pluginList) { plugin.updateResources(newResources); } return newResources; } public static void hookResources(Context base, Resources resources) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return; } try { Reflector reflector = Reflector.with(base); reflector.field("mResources").set(resources); Object loadedApk = reflector.field("mPackageInfo").get(); Reflector.with(loadedApk).field("mResources").set(resources); Object activityThread = ActivityThread.currentActivityThread(); Object resManager; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { resManager = android.app.ResourcesManager.getInstance(); } else { resManager = Reflector.with(activityThread).field("mResourcesManager").get(); } Map> map = Reflector.with(resManager).field("mActiveResources").get(); Object key = map.keySet().iterator().next(); map.put(key, new WeakReference<>(resources)); } catch (Exception e) { e.printStackTrace(); } } /** * Use System Apis to update all existing resources. *
* 1. Update ApplicationInfo.splitSourceDirs and LoadedApk.mSplitResDirs *
* 2. Replace all keys of ResourcesManager.mResourceImpls to new ResourcesKey *
* 3. Use ResourcesManager.appendLibAssetForMainAssetPath(appInfo.publicSourceDir, "${packageName}.vastub") to update all existing resources. *
* * see android.webkit.WebViewDelegate.addWebViewAssetPath(Context) */ @TargetApi(Build.VERSION_CODES.N) private static Resources createResourcesForN(Context context, String packageName, File apk) throws Exception { long startTime = System.currentTimeMillis(); String newAssetPath = apk.getAbsolutePath(); ApplicationInfo info = context.getApplicationInfo(); String baseResDir = info.publicSourceDir; info.splitSourceDirs = append(info.splitSourceDirs, newAssetPath); LoadedApk loadedApk = Reflector.with(context).field("mPackageInfo").get(); Reflector rLoadedApk = Reflector.with(loadedApk).field("mSplitResDirs"); String[] splitResDirs = rLoadedApk.get(); rLoadedApk.set(append(splitResDirs, newAssetPath)); final android.app.ResourcesManager resourcesManager = android.app.ResourcesManager.getInstance(); ArrayMap> originalMap = Reflector.with(resourcesManager).field("mResourceImpls").get(); synchronized (resourcesManager) { HashMap> resolvedMap = new HashMap<>(); if (Build.VERSION.SDK_INT >= 28 || (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // P Preview ResourcesManagerCompatForP.resolveResourcesImplMap(originalMap, resolvedMap, context, loadedApk); } else { ResourcesManagerCompatForN.resolveResourcesImplMap(originalMap, resolvedMap, baseResDir, newAssetPath); } originalMap.clear(); originalMap.putAll(resolvedMap); } android.app.ResourcesManager.getInstance().appendLibAssetForMainAssetPath(baseResDir, packageName + ".vastub"); Resources newResources = context.getResources(); // lastly, sync all LoadedPlugin to newResources for (LoadedPlugin plugin : PluginManager.getInstance(context).getAllLoadedPlugins()) { plugin.updateResources(newResources); } Log.d(TAG, "createResourcesForN cost time: +" + (System.currentTimeMillis() - startTime) + "ms"); return newResources; } private static String[] append(String[] paths, String newPath) { if (contains(paths, newPath)) { return paths; } final int newPathsCount = 1 + (paths != null ? paths.length : 0); final String[] newPaths = new String[newPathsCount]; if (paths != null) { System.arraycopy(paths, 0, newPaths, 0, paths.length); } newPaths[newPathsCount - 1] = newPath; return newPaths; } @TargetApi(Build.VERSION_CODES.KITKAT) private static boolean contains(String[] array, String value) { if (array == null) { return false; } for (int i = 0; i < array.length; i++) { if (Objects.equals(array[i], value)) { return true; } } return false; } private static boolean isMiUi(Resources resources) { return resources.getClass().getName().equals("android.content.res.MiuiResources"); } private static boolean isVivo(Resources resources) { return resources.getClass().getName().equals("android.content.res.VivoResources"); } private static boolean isNubia(Resources resources) { return resources.getClass().getName().equals("android.content.res.NubiaResources"); } private static boolean isNotRawResources(Resources resources) { return !resources.getClass().getName().equals("android.content.res.Resources"); } private static final class MiUiResourcesCompat { private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception { Reflector reflector = Reflector.on("android.content.res.MiuiResources"); Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class) .newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); return newResources; } } private static final class VivoResourcesCompat { private static Resources createResources(Context hostContext, Resources hostResources, AssetManager assetManager) throws Exception { Reflector reflector = Reflector.on("android.content.res.VivoResources"); Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class) .newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); reflector.method("init", String.class).callByCaller(newResources, hostContext.getPackageName()); reflector.field("mThemeValues"); reflector.set(newResources, reflector.get(hostResources)); return newResources; } } private static final class NubiaResourcesCompat { private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception { Reflector reflector = Reflector.on("android.content.res.NubiaResources"); Resources newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class) .newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); return newResources; } } private static final class AdaptationResourcesCompat { private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception { Resources newResources; try { Reflector reflector = Reflector.with(hostResources); newResources = reflector.constructor(AssetManager.class, DisplayMetrics.class, Configuration.class) .newInstance(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } catch (Exception e) { newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } return newResources; } } private static final class ResourcesManagerCompatForN { @TargetApi(Build.VERSION_CODES.KITKAT) public static void resolveResourcesImplMap(Map> originalMap, Map> resolvedMap, String baseResDir, String newAssetPath) throws Exception { for (Map.Entry> entry : originalMap.entrySet()) { ResourcesKey key = entry.getKey(); if (Objects.equals(key.mResDir, baseResDir)) { resolvedMap.put(new ResourcesKey(key.mResDir, append(key.mSplitResDirs, newAssetPath), key.mOverlayDirs, key.mLibDirs, key.mDisplayId, key.mOverrideConfiguration, key.mCompatInfo), entry.getValue()); } else { resolvedMap.put(key, entry.getValue()); } } } } private static final class ResourcesManagerCompatForP { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static void resolveResourcesImplMap(Map> originalMap, Map> resolvedMap, Context context, LoadedApk loadedApk) throws Exception { HashMap newResImplMap = new HashMap<>(); Map resKeyMap = new HashMap<>(); Resources newRes; // Recreate the resImpl of the context // See LoadedApk.getResources() if (mDefaultConfiguration == null) { mDefaultConfiguration = new Configuration(); } newRes = context.createConfigurationContext(mDefaultConfiguration).getResources(); newResImplMap.put(newRes.getImpl(), context); // Recreate the ResImpl of the activity for (WeakReference ref : PluginManager.getInstance(context).getInstrumentation().getActivities()) { Activity activity = ref.get(); if (activity != null) { newRes = activity.createConfigurationContext(activity.getResources().getConfiguration()).getResources(); newResImplMap.put(newRes.getImpl(), activity); } } // Mapping all resKey and resImpl for (Map.Entry> entry : originalMap.entrySet()) { ResourcesImpl resImpl = entry.getValue().get(); if (resImpl != null) { resKeyMap.put(resImpl, entry.getKey()); } resolvedMap.put(entry.getKey(), entry.getValue()); } // Replace the resImpl to the new resKey and remove the origin resKey for (Map.Entry entry : newResImplMap.entrySet()) { ResourcesKey newKey = resKeyMap.get(entry.getKey()); ResourcesImpl originResImpl = entry.getValue().getResources().getImpl(); resolvedMap.put(newKey, new WeakReference<>(originResImpl)); resolvedMap.remove(resKeyMap.get(originResImpl)); } } } }