package android.content.res; import android.content.Context; import android.content.pm.PackageParser; import android.content.pm.PackageParser.PackageParserException; import android.graphics.Color; import android.graphics.Movie; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Html; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.elderdrivers.riru.edxp.config.EdXpConfigGlobal; import org.xmlpull.v1.XmlPullParser; import java.io.File; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.WeakHashMap; import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodHook.MethodHookParam; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; import de.robv.android.xposed.callbacks.XC_LayoutInflated; import de.robv.android.xposed.callbacks.XC_LayoutInflated.LayoutInflatedParam; import de.robv.android.xposed.callbacks.XCallback; import static de.robv.android.xposed.XposedHelpers.decrementMethodDepth; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedHelpers.getIntField; import static de.robv.android.xposed.XposedHelpers.getLongField; import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.incrementMethodDepth; /** * {@link android.content.res.Resources} subclass that allows replacing individual resources. * *

Xposed replaces the standard resources with this class, which overrides the methods used for * retrieving individual resources and adds possibilities to replace them. These replacements can * be set using the methods made available via the API methods in this class. */ @SuppressWarnings("JniMissingFunction") public class XResources extends Resources { private static final SparseArray> sReplacements = new SparseArray<>(); private static final SparseArray> sResourceNames = new SparseArray<>(); private static final byte[] sSystemReplacementsCache = new byte[256]; // bitmask: 0x000700ff => 2048 bit => 256 bytes private byte[] mReplacementsCache; // bitmask: 0x0007007f => 1024 bit => 128 bytes private static final HashMap sReplacementsCacheMap = new HashMap<>(); private static final SparseArray sColorStateListCache = new SparseArray<>(0); private static final SparseArray>> sLayoutCallbacks = new SparseArray<>(); private static final WeakHashMap sXmlInstanceDetails = new WeakHashMap<>(); private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails"; private static final ThreadLocal> sIncludedLayouts = new ThreadLocal>() { @Override protected LinkedList initialValue() { return new LinkedList<>(); } }; private static final HashMap sResDirLastModified = new HashMap<>(); private static final HashMap sResDirPackageNames = new HashMap<>(); private static ThreadLocal sLatestResKey = null; private boolean mIsObjectInited; private String mResDir; private String mPackageName; public XResources(AssetManager assets, DisplayMetrics metrics, Configuration config) { super(assets, metrics, config); } public XResources(ClassLoader classLoader) { super(classLoader); } /** Dummy, will never be called (objects are transferred to this class only). */ // private XResources() { // throw new UnsupportedOperationException(); // } /** @hide */ public void initObject(String resDir) { if (mIsObjectInited) throw new IllegalStateException("Object has already been initialized"); this.mResDir = resDir; this.mPackageName = getPackageName(resDir); if (resDir != null) { synchronized (sReplacementsCacheMap) { mReplacementsCache = sReplacementsCacheMap.get(resDir); if (mReplacementsCache == null) { mReplacementsCache = new byte[128]; sReplacementsCacheMap.put(resDir, mReplacementsCache); } } } this.mIsObjectInited = true; } /** @hide */ public boolean isFirstLoad() { synchronized (sReplacements) { if (mResDir == null) return false; Long lastModification = new File(mResDir).lastModified(); Long oldModified = sResDirLastModified.get(mResDir); if (lastModification.equals(oldModified)) return false; sResDirLastModified.put(mResDir, lastModification); if (oldModified == null) return true; // file was changed meanwhile => remove old replacements for (int i = 0; i < sReplacements.size(); i++) { sReplacements.valueAt(i).remove(mResDir); } Arrays.fill(mReplacementsCache, (byte) 0); return true; } } /** @hide */ public static void setPackageNameForResDir(String packageName, String resDir) { synchronized (sResDirPackageNames) { sResDirPackageNames.put(resDir, packageName); } } /** * Returns the name of the package that these resources belong to, or "android" for system resources. */ public String getPackageName() { return mPackageName; } private static String getPackageName(String resDir) { if (resDir == null) return "android"; String packageName; synchronized (sResDirPackageNames) { packageName = sResDirPackageNames.get(resDir); } if (packageName != null) return packageName; PackageParser.PackageLite pkgInfo; if (Build.VERSION.SDK_INT >= 21) { try { pkgInfo = PackageParser.parsePackageLite(new File(resDir), 0); } catch (PackageParserException e) { throw new IllegalStateException("Could not determine package name for " + resDir, e); } } else { pkgInfo = PackageParser.parsePackageLite(resDir, 0); } if (pkgInfo != null && pkgInfo.packageName != null) { // Log.w(XposedBridge.TAG, "Package name for " + resDir + " had to be retrieved via parser"); packageName = pkgInfo.packageName; setPackageNameForResDir(packageName, resDir); return packageName; } throw new IllegalStateException("Could not determine package name for " + resDir); } /** * Special case of {@link #getPackageName} during object creation. * *

For a short moment during/after the creation of a new {@link android.content.res Resources} * object, it isn't an instance of {@link XResources} yet. For any hooks that need information * about the just created object during this particular stage, this method will return the * package name. * *

If you call this method outside of {@code getTopLevelResources()}, it * throws an {@code IllegalStateException}. */ public static String getPackageNameDuringConstruction() { Object key; if (sLatestResKey == null || (key = sLatestResKey.get()) == null) throw new IllegalStateException("This method can only be called during getTopLevelResources()"); String resDir = (String) getObjectField(key, "mResDir"); return getPackageName(resDir); } /** @hide */ public static void init(ThreadLocal latestResKey) throws Exception { sLatestResKey = latestResKey; findAndHookMethod(LayoutInflater.class, "inflate", XmlPullParser.class, ViewGroup.class, boolean.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { if (param.hasThrowable()) return; XMLInstanceDetails details; synchronized (sXmlInstanceDetails) { details = sXmlInstanceDetails.get(param.args[0]); } if (details != null) { LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); liparam.view = (View) param.getResult(); liparam.resNames = details.resNames; liparam.variant = details.variant; liparam.res = details.res; XCallback.callAll(liparam); } } }); final XC_MethodHook parseIncludeHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { sIncludedLayouts.get().push(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { sIncludedLayouts.get().pop(); if (param.hasThrowable()) return; // filled in by our implementation of getLayout() XMLInstanceDetails details = (XMLInstanceDetails) param.getObjectExtra(EXTRA_XML_INSTANCE_DETAILS); if (details != null) { LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); ViewGroup group = (ViewGroup) param.args[(Build.VERSION.SDK_INT < 23) ? 1 : 2]; liparam.view = group.getChildAt(group.getChildCount() - 1); liparam.resNames = details.resNames; liparam.variant = details.variant; liparam.res = details.res; XCallback.callAll(liparam); } } }; if (Build.VERSION.SDK_INT < 21) { findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, View.class, AttributeSet.class, parseIncludeHook); } else if (Build.VERSION.SDK_INT < 23) { findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, View.class, AttributeSet.class, boolean.class, parseIncludeHook); } else { findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, Context.class, View.class, AttributeSet.class, parseIncludeHook); } } /** * Wrapper for information about an indiviual resource. */ public static class ResourceNames { /** The resource ID. */ public final int id; /** The resource package name as returned by {@link #getResourcePackageName}. */ public final String pkg; /** The resource entry name as returned by {@link #getResourceEntryName}. */ public final String name; /** The resource type name as returned by {@link #getResourceTypeName}. */ public final String type; /** The full resource nameas returned by {@link #getResourceName}. */ public final String fullName; private ResourceNames(int id, String pkg, String name, String type) { this.id = id; this.pkg = pkg; this.name = name; this.type = type; this.fullName = pkg + ":" + type + "/" + name; } /** * Returns whether all non-null parameters match the values of this object. */ public boolean equals(String pkg, String name, String type, int id) { return (pkg == null || pkg.equals(this.pkg)) && (name == null || name.equals(this.name)) && (type == null || type.equals(this.type)) && (id == 0 || id == this.id); } } private ResourceNames getResourceNames(int id) { return new ResourceNames( id, getResourcePackageName(id), getResourceTypeName(id), getResourceEntryName(id)); } private static ResourceNames getSystemResourceNames(int id) { Resources sysRes = getSystem(); return new ResourceNames( id, sysRes.getResourcePackageName(id), sysRes.getResourceTypeName(id), sysRes.getResourceEntryName(id)); } private static void putResourceNames(String resDir, ResourceNames resNames) { int id = resNames.id; synchronized (sResourceNames) { HashMap inner = sResourceNames.get(id); if (inner == null) { inner = new HashMap<>(); sResourceNames.put(id, inner); } synchronized (inner) { inner.put(resDir, resNames); } } } // ======================================================= // DEFINING REPLACEMENTS // ======================================================= /** * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. * * @param id The ID of the resource which should be replaced. * @param replacement The replacement, see above. */ public void setReplacement(int id, Object replacement) { setReplacement(id, replacement, this); } /** * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. * * @deprecated Use {@link #setReplacement(String, String, String, Object)} instead. * * @param fullName The full resource name, e.g. {@code com.example.myapplication:string/app_name}. * See {@link #getResourceName}. * @param replacement The replacement. */ @Deprecated public void setReplacement(String fullName, Object replacement) { int id = getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); setReplacement(id, replacement, this); } /** * Sets a replacement for an individual resource. If called more than once for the same ID, the * replacement from the last call is used. Setting the replacement to {@code null} removes it. * *

The allowed replacements depend on the type of the source. All types accept an * {@link XResForwarder} object, which is usually created with {@link XModuleResources#fwd}. * The resource request will then be forwarded to another {@link android.content.res.Resources} * object. In addition to that, the following replacement types are accepted: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Resource type Additional allowed replacement types (*) Returned from (**)
Animation none{@link #getAnimation}
Bool{@link Boolean}{@link #getBoolean}
Color{@link Integer} (you might want to use {@link Color#parseColor}){@link #getColor}
* {@link #getDrawable} (creates a {@link ColorDrawable})
* {@link #getColorStateList} (calls {@link android.content.res.ColorStateList#valueOf}) *
Color State List{@link android.content.res.ColorStateList}
* {@link Integer} (calls {@link android.content.res.ColorStateList#valueOf}) *
{@link #getColorStateList}
Dimension{@link DimensionReplacement} (since v50){@link #getDimension}
* {@link #getDimensionPixelOffset}
* {@link #getDimensionPixelSize} *
Drawable * (including mipmap){@link DrawableLoader}
* {@link Integer} (creates a {@link ColorDrawable}) *
{@link #getDrawable}
* {@link #getDrawableForDensity} *
Fraction none{@link #getFraction}
Integer{@link Integer}{@link #getInteger}
Integer Array{@code int[]}{@link #getIntArray}
Layout none, but see {@link #hookLayout}{@link #getLayout}
Movie none{@link #getMovie}
Quantity Strings (Plurals) none{@link #getQuantityString}
* {@link #getQuantityText} *
String{@link String}
* {@link CharSequence} (for styled texts, see also {@link Html#fromHtml}) *
{@link #getString}
* {@link #getText} *
String Array{@code String[]}
* {@code CharSequence[]} (for styled texts, see also {@link Html#fromHtml}) *
{@link #getStringArray}
* {@link #getTextArray} *
XML none{@link #getXml}
* {@link #getQuantityText} *
* *

Other resource types, such as * styles/themes, * {@linkplain #openRawResource raw resources} and * typed arrays * can't be replaced. * *

* * Auto-boxing allows you to use literals like {@code 123} where an {@link Integer} is * accepted, so you don't neeed to call methods like {@link Integer#valueOf(int)} manually.
* ** Some of these methods have multiple variants, only one of them is mentioned here. *
* * @param pkg The package name, e.g. {@code com.example.myapplication}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code string}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code app_name}. * See {@link #getResourceEntryName}. * @param replacement The replacement. */ public void setReplacement(String pkg, String type, String name, Object replacement) { int id = getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); setReplacement(id, replacement, this); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * See {@link #setSystemWideReplacement(String, String, String, Object)}. * * @param id The ID of the resource which should be replaced. * @param replacement The replacement. */ public static void setSystemWideReplacement(int id, Object replacement) { setReplacement(id, replacement, null); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * See {@link #setSystemWideReplacement(String, String, String, Object)}. * * @deprecated Use {@link #setSystemWideReplacement(String, String, String, Object)} instead. * * @param fullName The full resource name, e.g. {@code android:string/yes}. * See {@link #getResourceName}. * @param replacement The replacement. */ @Deprecated public static void setSystemWideReplacement(String fullName, Object replacement) { int id = getSystem().getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); setReplacement(id, replacement, null); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * *

Some resources are part of the Android framework and can be used in any app. They're * accessible via {@link android.R android.R} and are not bound to a specific * {@link android.content.res.Resources} instance. Such resources can be replaced in * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no * {@link XResources} object easily available in that scope, this static method can be used * to set resource replacements. All other details (e.g. how certain types can be replaced) are * mentioned in {@link #setReplacement(String, String, String, Object)}. * * @param pkg The package name, should always be {@code android} here. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code string}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code yes}. * See {@link #getResourceEntryName}. * @param replacement The replacement. */ public static void setSystemWideReplacement(String pkg, String type, String name, Object replacement) { int id = getSystem().getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); setReplacement(id, replacement, null); } private static void setReplacement(int id, Object replacement, XResources res) { String resDir = (res != null) ? res.mResDir : null; if (id == 0) throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); else if (resDir == null && id >= 0x7f000000) throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); if (replacement instanceof Drawable) throw new IllegalArgumentException("Drawable replacements are deprecated since Xposed 2.1. Use DrawableLoader instead."); // Cache that we have a replacement for this ID, false positives are accepted to save memory. if (id < 0x7f000000) { int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; synchronized (sSystemReplacementsCache) { sSystemReplacementsCache[cacheKey] |= 1 << (id & 7); } } else { int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; synchronized (res.mReplacementsCache) { res.mReplacementsCache[cacheKey] |= 1 << (id & 7); } } synchronized (sReplacements) { HashMap inner = sReplacements.get(id); if (inner == null) { inner = new HashMap<>(); sReplacements.put(id, inner); } inner.put(resDir, replacement); } } // ======================================================= // RETURNING REPLACEMENTS // ======================================================= private Object getReplacement(int id) { if (id <= 0) return null; // Check the cache whether it's worth looking for replacements if (id < 0x7f000000) { int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; if ((sSystemReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) return null; } else if (mResDir != null) { int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; if ((mReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) return null; } HashMap inner; synchronized (sReplacements) { inner = sReplacements.get(id); } if (inner == null) return null; synchronized (inner) { Object result = inner.get(mResDir); if (result != null || mResDir == null) return result; return inner.get(null); } } /** @hide */ @Override public XmlResourceParser getAnimation(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); XmlResourceParser result = repRes.getAnimation(repId); if (!loadedFromCache) { long parseState = (Build.VERSION.SDK_INT >= 21) ? getLongField(result, "mParseState") : getIntField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } return result; } return super.getAnimation(id); } /** @hide */ @Override public boolean getBoolean(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Boolean) { return (Boolean) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getBoolean(repId); } return super.getBoolean(id); } /** @hide */ @Override public int getColor(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColor(repId); } return super.getColor(id); } /** @hide */ @Override public ColorStateList getColorStateList(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof ColorStateList) { return (ColorStateList) replacement; } else if (replacement instanceof Integer) { int color = (Integer) replacement; synchronized (sColorStateListCache) { ColorStateList result = sColorStateListCache.get(color); if (result == null) { result = ColorStateList.valueOf(color); sColorStateListCache.put(color, result); } return result; } } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColorStateList(repId); } return super.getColorStateList(id); } /** @hide */ @Override public float getDimension(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimension(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimension(repId); } return super.getDimension(id); } /** @hide */ @Override public int getDimensionPixelOffset(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimensionPixelOffset(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelOffset(repId); } return super.getDimensionPixelOffset(id); } /** @hide */ @Override public int getDimensionPixelSize(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimensionPixelSize(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getDimensionPixelSize(id); } /** @hide */ @Override public Drawable getDrawable(int id) throws NotFoundException { try { if (incrementMethodDepth("getDrawable") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } } return super.getDrawable(id); } finally { decrementMethodDepth("getDrawable"); } } /** @hide */ @Override public Drawable getDrawable(int id, Theme theme) throws NotFoundException { try { if (incrementMethodDepth("getDrawable") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } } return super.getDrawable(id, theme); } finally { decrementMethodDepth("getDrawable"); } } /** @hide */ @Override public Drawable getDrawable(int id, Theme theme, boolean supportComposedIcons) throws NotFoundException { try { if (incrementMethodDepth("getDrawable") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } } return super.getDrawable(id, theme, supportComposedIcons); } finally { decrementMethodDepth("getDrawable"); } } /** @hide */ @Override public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { try { if (incrementMethodDepth("getDrawableForDensity") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawableForDensity(repId, density); } } return super.getDrawableForDensity(id, density); } finally { decrementMethodDepth("getDrawableForDensity"); } } /** @hide */ @Override public Drawable getDrawableForDensity(int id, int density, Theme theme) throws NotFoundException { try { if (incrementMethodDepth("getDrawableForDensity") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawableForDensity(repId, density); } } return super.getDrawableForDensity(id, density, theme); } finally { decrementMethodDepth("getDrawableForDensity"); } } /** @hide */ @Override public Drawable getDrawableForDensity(int id, int density, Theme theme, boolean supportComposedIcons) throws NotFoundException { try { if (incrementMethodDepth("getDrawableForDensity") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawableForDensity(repId, density); } } return super.getDrawableForDensity(id, density, theme, supportComposedIcons); } finally { decrementMethodDepth("getDrawableForDensity"); } } /** @hide */ @Override public float getFraction(int id, int base, int pbase) { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getFraction(repId, base, pbase); } return super.getFraction(id, base, pbase); } /** @hide */ @Override public int getInteger(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInteger(id); } /** @hide */ @Override public int[] getIntArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof int[]) { return (int[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getIntArray(repId); } return super.getIntArray(id); } /** @hide */ @Override public XmlResourceParser getLayout(int id) throws NotFoundException { XmlResourceParser result; Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); result = repRes.getLayout(repId); if (!loadedFromCache) { long parseState = (Build.VERSION.SDK_INT >= 21) ? getLongField(result, "mParseState") : getIntField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } } else { result = super.getLayout(id); } // Check whether this layout is hooked HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); } if (inner != null) { CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(mResDir); if (callbacks == null && mResDir != null) callbacks = inner.get(null); } if (callbacks != null) { String variant = "layout"; TypedValue value = (TypedValue) getObjectField(this, "mTmpValue"); getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { String[] components = value.string.toString().split("/", 3); if (components.length == 3) variant = components[1]; else XposedBridge.log("Unexpected resource path \"" + value.string.toString() + "\" for resource id 0x" + Integer.toHexString(id)); } else { XposedBridge.log(new NotFoundException("Could not find file name for resource id 0x") + Integer.toHexString(id)); } synchronized (sXmlInstanceDetails) { synchronized (sResourceNames) { HashMap resNamesInner = sResourceNames.get(id); if (resNamesInner != null) { synchronized (resNamesInner) { XMLInstanceDetails details = new XMLInstanceDetails(resNamesInner.get(mResDir), variant, callbacks); sXmlInstanceDetails.put(result, details); // if we were called inside LayoutInflater.parseInclude, store the details for it MethodHookParam top = sIncludedLayouts.get().peek(); if (top != null) top.setObjectExtra(EXTRA_XML_INSTANCE_DETAILS, details); } } } } } } return result; } /** @hide */ @Override public Movie getMovie(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getMovie(repId); } return super.getMovie(id); } /** @hide */ @Override public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getQuantityText(repId, quantity); } return super.getQuantityText(id, quantity); } // these are handled by getQuantityText: // public String getQuantityString(int id, int quantity); // public String getQuantityString(int id, int quantity, Object... formatArgs); /** @hide */ @Override public String[] getStringArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof String[]) { return (String[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getStringArray(repId); } return super.getStringArray(id); } /** @hide */ @Override public CharSequence getText(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId); } return super.getText(id); } // these are handled by getText: // public String getString(int id); // public String getString(int id, Object... formatArgs); /** @hide */ @Override public CharSequence getText(int id, CharSequence def) { Object replacement = getReplacement(id); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId, def); } return super.getText(id, def); } /** @hide */ @Override public CharSequence[] getTextArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof CharSequence[]) { return (CharSequence[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getTextArray(repId); } return super.getTextArray(id); } /** @hide */ @Override public XmlResourceParser getXml(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); XmlResourceParser result = repRes.getXml(repId); if (!loadedFromCache) { long parseState = (Build.VERSION.SDK_INT >= 21) ? getLongField(result, "mParseState") : getIntField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } return result; } return super.getXml(id); } private static boolean isXmlCached(Resources res, int id) { int[] mCachedXmlBlockIds = (int[]) getObjectField(getObjectField(res, "mResourcesImpl"), "mCachedXmlBlockCookies"); synchronized (mCachedXmlBlockIds) { for (int cachedId : mCachedXmlBlockIds) { if (cachedId == id) return true; } } return false; } private static void rewriteXmlReferencesNative(long parserPtr, XResources origRes, Resources repRes) { EdXpConfigGlobal.getHookProvider().rewriteXmlReferencesNative(parserPtr, origRes, repRes); } /** * Used to replace reference IDs in XMLs. * * When resource requests are forwarded to modules, the may include references to resources with the same * name as in the original resources, but the IDs generated by aapt will be different. rewriteXmlReferencesNative * walks through all references and calls this function to find out the original ID, which it then writes to * the compiled XML file in the memory. */ private static int translateResId(int id, XResources origRes, Resources repRes) { try { String entryName = repRes.getResourceEntryName(id); String entryType = repRes.getResourceTypeName(id); String origPackage = origRes.mPackageName; int origResId = 0; try { // look for a resource with the same name and type in the original package origResId = origRes.getIdentifier(entryName, entryType, origPackage); } catch (NotFoundException ignored) {} boolean repResDefined = false; try { final TypedValue tmpValue = new TypedValue(); repRes.getValue(id, tmpValue, false); // if a resource has not been defined (i.e. only a resource ID has been created), it will equal "false" // this means a boolean "false" value is not detected of it is directly referenced in an XML file repResDefined = !(tmpValue.type == TypedValue.TYPE_INT_BOOLEAN && tmpValue.data == 0); } catch (NotFoundException ignored) {} if (!repResDefined && origResId == 0 && !entryType.equals("id")) { XposedBridge.log(entryType + "/" + entryName + " is neither defined in module nor in original resources"); return 0; } // exists only in module, so create a fake resource id if (origResId == 0) origResId = getFakeResId(repRes, id); // IDs will never be loaded, no need to set a replacement if (repResDefined && !entryType.equals("id")) origRes.setReplacement(origResId, new XResForwarder(repRes, id)); return origResId; } catch (Exception e) { XposedBridge.log(e); return id; } } /** * Generates a fake resource ID. * *

The parameter is just hashed, it doesn't have a deeper meaning. However, it's recommended * to use values with a low risk for conflicts, such as a full resource name. Calling this * method multiple times will return the same ID. * * @param resName A used for hashing, see above. * @return The fake resource ID. */ public static int getFakeResId(String resName) { return 0x7e000000 | (resName.hashCode() & 0x00ffffff); } /** * Generates a fake resource ID. * *

This variant uses the result of {@link #getResourceName} to create the hash that the ID is * based on. The given resource doesn't need to match the {@link XResources} instance for which * the fake resource ID is going to be used. * * @param res The {@link android.content.res.Resources} object to be used for hashing. * @param id The resource ID to be used for hashing. * @return The fake resource ID. */ public static int getFakeResId(Resources res, int id) { return getFakeResId(res.getResourceName(id)); } /** * Makes any individual resource available from another {@link android.content.res.Resources} * instance available in this {@link XResources} instance. * *

This method combines calls to {@link #getFakeResId(Resources, int)} and * {@link #setReplacement(int, Object)} to generate a fake resource ID and set up a replacement * for it which forwards to the given resource. * *

The returned ID can only be used to retrieve the resource, it won't work for methods like * {@link #getResourceName} etc. * * @param res The target {@link android.content.res.Resources} instance. * @param id The target resource ID. * @return The fake resource ID (see above). */ public int addResource(Resources res, int id) { int fakeId = getFakeResId(res, id); synchronized (sReplacements) { if (sReplacements.indexOfKey(fakeId) < 0) setReplacement(fakeId, new XResForwarder(res, id)); } return fakeId; } /** * Similar to {@link #translateResId}, but used to determine the original ID of attribute names. */ private static int translateAttrId(String attrName, XResources origRes) { String origPackage = origRes.mPackageName; int origAttrId = 0; try { origAttrId = origRes.getIdentifier(attrName, "attr", origPackage); } catch (NotFoundException e) { XposedBridge.log("Attribute " + attrName + " not found in original resources"); } return origAttrId; } // ======================================================= // XTypedArray class // ======================================================= /** * {@link android.content.res.TypedArray} replacement that replaces values on-the-fly. * Mainly used when inflating layouts. * @hide */ public static class XTypedArray extends TypedArray { public XTypedArray(Resources resources) { super(resources); } /** Dummy, will never be called (objects are transferred to this class only). */ // private XTypedArray() { // super(null, null, null, 0); // throw new UnsupportedOperationException(); // } @Override public boolean getBoolean(int index, boolean defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Boolean) { return (Boolean) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getBoolean(repId); } return super.getBoolean(index, defValue); } @Override public int getColor(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColor(repId); } return super.getColor(index, defValue); } @Override public ColorStateList getColorStateList(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof ColorStateList) { return (ColorStateList) replacement; } else if (replacement instanceof Integer) { int color = (Integer) replacement; synchronized (sColorStateListCache) { ColorStateList result = sColorStateListCache.get(color); if (result == null) { result = ColorStateList.valueOf(color); sColorStateListCache.put(color, result); } return result; } } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColorStateList(repId); } return super.getColorStateList(index); } @Override public float getDimension(int index, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimension(repId); } return super.getDimension(index, defValue); } @Override public int getDimensionPixelOffset(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelOffset(repId); } return super.getDimensionPixelOffset(index, defValue); } @Override public int getDimensionPixelSize(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getDimensionPixelSize(index, defValue); } @Override public Drawable getDrawable(int index) { final int resId = getResourceId(index, 0); XResources xres = (XResources) getResources(); Object replacement = xres.getReplacement(resId); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(xres, resId); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } return super.getDrawable(index); } @Override public float getFloat(int index, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); // dimensions seem to be the only way to define floats by references return repRes.getDimension(repId); } return super.getFloat(index, defValue); } @Override public float getFraction(int index, int base, int pbase, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); // dimensions seem to be the only way to define floats by references return repRes.getFraction(repId, base, pbase); } return super.getFraction(index, base, pbase, defValue); } @Override public int getInt(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInt(index, defValue); } @Override public int getInteger(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInteger(index, defValue); } @Override public int getLayoutDimension(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getLayoutDimension(index, defValue); } @Override public int getLayoutDimension(int index, String name) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getLayoutDimension(index, name); } @Override public String getString(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence) { return replacement.toString(); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getString(repId); } return super.getString(index); } @Override public CharSequence getText(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId); } return super.getText(index); } @Override public CharSequence[] getTextArray(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence[]) { return (CharSequence[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getTextArray(repId); } return super.getTextArray(index); } } // ======================================================= // DrawableLoader class // ======================================================= /** * Callback for drawable replacements. Instances of this class can passed to * {@link #setReplacement(String, String, String, Object)} and its variants. * *

Make sure to always return new {@link Drawable} instances, as drawables * usually can't be reused. */ @SuppressWarnings("UnusedParameters") public static abstract class DrawableLoader { /** * Constructor. */ public DrawableLoader() {} /** * Called when the hooked drawable resource has been requested. * * @param res The {@link XResources} object in which the hooked drawable resides. * @param id The resource ID which has been requested. * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. * @throws Throwable Everything the callback throws is caught and logged. */ public abstract Drawable newDrawable(XResources res, int id) throws Throwable; /** * Like {@link #newDrawable}, but called for {@link #getDrawableForDensity}. The default * implementation is to use the result of {@link #newDrawable}. * * @param res The {@link XResources} object in which the hooked drawable resides. * @param id The resource ID which has been requested. * @param density The desired screen density indicated by the resource as found in * {@link DisplayMetrics}. * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. * @throws Throwable Everything the callback throws is caught and logged. */ public Drawable newDrawableForDensity(XResources res, int id, int density) throws Throwable { return newDrawable(res, id); } } // ======================================================= // DimensionReplacement class // ======================================================= /** * Callback for dimension replacements. Instances of this class can passed to * {@link #setReplacement(String, String, String, Object)} and its variants. */ public static class DimensionReplacement { private final float mValue; private final int mUnit; /** * Creates an instance that can be used for {@link #setReplacement(String, String, String, Object)} * to replace a dimension resource. * * @param value The value of the replacement, in the unit specified with the next parameter. * @param unit One of the {@code COMPLEX_UNIT_*} constants in {@link TypedValue}. */ public DimensionReplacement(float value, int unit) { mValue = value; mUnit = unit; } /** Called by {@link android.content.res.Resources#getDimension}. */ public float getDimension(DisplayMetrics metrics) { return TypedValue.applyDimension(mUnit, mValue, metrics); } /** Called by {@link android.content.res.Resources#getDimensionPixelOffset}. */ public int getDimensionPixelOffset(DisplayMetrics metrics) { return (int) TypedValue.applyDimension(mUnit, mValue, metrics); } /** Called by {@link android.content.res.Resources#getDimensionPixelSize}. */ public int getDimensionPixelSize(DisplayMetrics metrics) { final float f = TypedValue.applyDimension(mUnit, mValue, metrics); final int res = (int)(f+0.5f); if (res != 0) return res; if (mValue == 0) return 0; if (mValue > 0) return 1; return -1; } } // ======================================================= // INFLATING LAYOUTS // ======================================================= private class XMLInstanceDetails { public final ResourceNames resNames; public final String variant; public final CopyOnWriteSortedSet callbacks; public final XResources res = XResources.this; private XMLInstanceDetails(ResourceNames resNames, String variant, CopyOnWriteSortedSet callbacks) { this.resNames = resNames; this.variant = variant; this.callbacks = callbacks; } } /** * Hook the inflation of a layout. * * @param id The ID of the resource which should be replaced. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public XC_LayoutInflated.Unhook hookLayout(int id, XC_LayoutInflated callback) { return hookLayoutInternal(mResDir, id, getResourceNames(id), callback); } /** * Hook the inflation of a layout. * * @deprecated Use {@link #hookLayout(String, String, String, XC_LayoutInflated)} instead. * * @param fullName The full resource name, e.g. {@code com.android.systemui:layout/statusbar}. * See {@link #getResourceName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ @Deprecated public XC_LayoutInflated.Unhook hookLayout(String fullName, XC_LayoutInflated callback) { int id = getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); return hookLayout(id, callback); } /** * Hook the inflation of a layout. * * @param pkg The package name, e.g. {@code com.android.systemui}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code layout}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code statusbar}. * See {@link #getResourceEntryName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public XC_LayoutInflated.Unhook hookLayout(String pkg, String type, String name, XC_LayoutInflated callback) { int id = getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); return hookLayout(id, callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. * * @param id The ID of the resource which should be replaced. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public static XC_LayoutInflated.Unhook hookSystemWideLayout(int id, XC_LayoutInflated callback) { if (id >= 0x7f000000) throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); return hookLayoutInternal(null, id, getSystemResourceNames(id), callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. * * @deprecated Use {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)} instead. * * @param fullName The full resource name, e.g. {@code android:layout/simple_list_item_1}. * See {@link #getResourceName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ @Deprecated public static XC_LayoutInflated.Unhook hookSystemWideLayout(String fullName, XC_LayoutInflated callback) { int id = getSystem().getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); return hookSystemWideLayout(id, callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * *

Some layouts are part of the Android framework and can be used in any app. They're * accessible via {@link android.R.layout android.R.layout} and are not bound to a specific * {@link android.content.res.Resources} instance. Such resources can be replaced in * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no * {@link XResources} object easily available in that scope, this static method can be used * to hook layouts. * * @param pkg The package name, e.g. {@code android}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code layout}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code simple_list_item_1}. * See {@link #getResourceEntryName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public static XC_LayoutInflated.Unhook hookSystemWideLayout(String pkg, String type, String name, XC_LayoutInflated callback) { int id = getSystem().getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); return hookSystemWideLayout(id, callback); } private static XC_LayoutInflated.Unhook hookLayoutInternal(String resDir, int id, ResourceNames resNames, XC_LayoutInflated callback) { if (id == 0) throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); if (inner == null) { inner = new HashMap<>(); sLayoutCallbacks.put(id, inner); } } CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(resDir); if (callbacks == null) { callbacks = new CopyOnWriteSortedSet<>(); inner.put(resDir, callbacks); } } callbacks.add(callback); putResourceNames(resDir, resNames); return callback.new Unhook(resDir, id); } /** @hide */ public static void unhookLayout(String resDir, int id, XC_LayoutInflated callback) { HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); if (inner == null) return; } CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(resDir); if (callbacks == null) return; } callbacks.remove(callback); } }