1754 lines
64 KiB
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> <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> <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> <i>none, but see {@link #hookLayout}</i></td>
|
|
* <td>{@link #getLayout}</td>
|
|
* </tr>
|
|
*
|
|
* <tr><td>Movie</td>
|
|
* <td> <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> <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> <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);
|
|
}
|
|
}
|