LSPosed/xposed-bridge/src/main/java/android/content/res/XResources.java

1754 lines
64 KiB
Java

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.
*
* <p>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<HashMap<String, Object>> sReplacements = new SparseArray<>();
private static final SparseArray<HashMap<String, ResourceNames>> 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<String, byte[]> sReplacementsCacheMap = new HashMap<>();
private static final SparseArray<ColorStateList> sColorStateListCache = new SparseArray<>(0);
private static final SparseArray<HashMap<String, CopyOnWriteSortedSet<XC_LayoutInflated>>> sLayoutCallbacks = new SparseArray<>();
private static final WeakHashMap<XmlResourceParser, XMLInstanceDetails> sXmlInstanceDetails = new WeakHashMap<>();
private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails";
private static final ThreadLocal<LinkedList<MethodHookParam>> sIncludedLayouts = new ThreadLocal<LinkedList<MethodHookParam>>() {
@Override
protected LinkedList<MethodHookParam> initialValue() {
return new LinkedList<>();
}
};
private static final HashMap<String, Long> sResDirLastModified = new HashMap<>();
private static final HashMap<String, String> sResDirPackageNames = new HashMap<>();
private static ThreadLocal<Object> 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.
*
* <p>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.
*
* <p class="warning">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<Object> 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<String, ResourceNames> 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.
*
* <p>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:
*
* <table>
* <thead>
* <tr><th>Resource type</th> <th>Additional allowed replacement types (*)</th> <th>Returned from (**)</th></tr>
* </thead>
*
* <tbody>
* <tr><td><a href="http://developer.android.com/guide/topics/resources/animation-resource.html">Animation</a></td>
* <td>&nbsp;<i>none</i></td>
* <td>{@link #getAnimation}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/more-resources.html#Bool">Bool</a></td>
* <td>{@link Boolean}</td>
* <td>{@link #getBoolean}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/more-resources.html#Color">Color</a></td>
* <td>{@link Integer} (you might want to use {@link Color#parseColor})</td>
* <td>{@link #getColor}<br>
* {@link #getDrawable} (creates a {@link ColorDrawable})<br>
* {@link #getColorStateList} (calls {@link android.content.res.ColorStateList#valueOf})
* </td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/color-list-resource.html">Color State List</a></td>
* <td>{@link android.content.res.ColorStateList}<br>
* {@link Integer} (calls {@link android.content.res.ColorStateList#valueOf})
* </td>
* <td>{@link #getColorStateList}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/more-resources.html#Dimension">Dimension</a></td>
* <td>{@link DimensionReplacement} <i>(since v50)</i></td>
* <td>{@link #getDimension}<br>
* {@link #getDimensionPixelOffset}<br>
* {@link #getDimensionPixelSize}
* </td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/drawable-resource.html">Drawable</a>
* (including <a href="http://developer.android.com/tools/projects/index.html#mipmap">mipmap</a>)</td>
* <td>{@link DrawableLoader}<br>
* {@link Integer} (creates a {@link ColorDrawable})
* </td>
* <td>{@link #getDrawable}<br>
* {@link #getDrawableForDensity}
* </td>
* </tr>
*
* <tr><td>Fraction</td>
* <td>&nbsp;<i>none</i></td>
* <td>{@link #getFraction}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/more-resources.html#Integer">Integer</a></td>
* <td>{@link Integer}</td>
* <td>{@link #getInteger}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/more-resources.html#IntegerArray">Integer Array</a></td>
* <td>{@code int[]}</td>
* <td>{@link #getIntArray}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/layout-resource.html">Layout</a></td>
* <td>&nbsp;<i>none, but see {@link #hookLayout}</i></td>
* <td>{@link #getLayout}</td>
* </tr>
*
* <tr><td>Movie</td>
* <td>&nbsp;<i>none</i></td>
* <td>{@link #getMovie}</td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">Quantity Strings (Plurals)</a></td>
* <td>&nbsp;<i>none</i></td>
* <td>{@link #getQuantityString}<br>
* {@link #getQuantityText}
* </td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/string-resource.html#String">String</a></td>
* <td>{@link String}<br>
* {@link CharSequence} (for styled texts, see also {@link Html#fromHtml})
* </td>
* <td>{@link #getString}<br>
* {@link #getText}
* </td>
* </tr>
*
* <tr><td><a href="http://developer.android.com/guide/topics/resources/string-resource.html#StringArray">String Array</a></td>
* <td>{@code String[]}<br>
* {@code CharSequence[]} (for styled texts, see also {@link Html#fromHtml})
* </td>
* <td>{@link #getStringArray}<br>
* {@link #getTextArray}
* </td>
* </tr>
*
* <tr><td>XML</td>
* <td>&nbsp;<i>none</i></td>
* <td>{@link #getXml}<br>
* {@link #getQuantityText}
* </td>
* </tr>
*
* </tbody>
* </table>
*
* <p>Other resource types, such as
* <a href="http://developer.android.com/guide/topics/resources/style-resource.html">styles/themes</a>,
* {@linkplain #openRawResource raw resources} and
* <a href="http://developer.android.com/guide/topics/resources/more-resources.html#TypedArray">typed arrays</a>
* can't be replaced.
*
* <p><i>
* * 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.<br>
* ** Some of these methods have multiple variants, only one of them is mentioned here.
* </i>
*
* @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).
*
* <p>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<String, Object> 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<String, Object> 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<String, CopyOnWriteSortedSet<XC_LayoutInflated>> inner;
synchronized (sLayoutCallbacks) {
inner = sLayoutCallbacks.get(id);
}
if (inner != null) {
CopyOnWriteSortedSet<XC_LayoutInflated> 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<String, ResourceNames> 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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p class="caution">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<XC_LayoutInflated> callbacks;
public final XResources res = XResources.this;
private XMLInstanceDetails(ResourceNames resNames, String variant, CopyOnWriteSortedSet<XC_LayoutInflated> 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).
*
* <p>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<String, CopyOnWriteSortedSet<XC_LayoutInflated>> inner;
synchronized (sLayoutCallbacks) {
inner = sLayoutCallbacks.get(id);
if (inner == null) {
inner = new HashMap<>();
sLayoutCallbacks.put(id, inner);
}
}
CopyOnWriteSortedSet<XC_LayoutInflated> 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<String, CopyOnWriteSortedSet<XC_LayoutInflated>> inner;
synchronized (sLayoutCallbacks) {
inner = sLayoutCallbacks.get(id);
if (inner == null)
return;
}
CopyOnWriteSortedSet<XC_LayoutInflated> callbacks;
synchronized (inner) {
callbacks = inner.get(resDir);
if (callbacks == null)
return;
}
callbacks.remove(callback);
}
}