blob: f1279925055cde8b35be8795110f881ba6cc6aaa [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.res;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ArrayResourceValue;
import com.android.ide.common.rendering.api.AssetRepository;
import com.android.ide.common.rendering.api.DensityBasedResourceValue;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.LayoutlibCallback;
import com.android.ide.common.rendering.api.PluralsResourceValue;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceNamespace;
import com.android.ide.common.rendering.api.ResourceNamespace.Resolver;
import com.android.ide.common.rendering.api.ResourceReference;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.ResourceValueImpl;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.BridgeConstants;
import com.android.layoutlib.bridge.android.BridgeContext;
import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
import com.android.layoutlib.bridge.android.UnresolvedResourceValue;
import com.android.layoutlib.bridge.impl.ParserFactory;
import com.android.layoutlib.bridge.impl.ResourceHelper;
import com.android.layoutlib.bridge.util.NinePatchInputStream;
import com.android.ninepatch.NinePatch;
import com.android.resources.ResourceType;
import com.android.resources.ResourceUrl;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import com.android.tools.layoutlib.annotations.VisibleForTesting;
import com.android.util.Pair;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.icu.text.PluralRules;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.util.TypedValue;
import android.view.DisplayAdjustments;
import android.view.ViewGroup.LayoutParams;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.WeakHashMap;
import static android.content.res.AssetManager.ACCESS_STREAMING;
import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
@SuppressWarnings("deprecation")
public class Resources_Delegate {
private static WeakHashMap<Resources, LayoutlibCallback> sLayoutlibCallbacks =
new WeakHashMap<>();
private static WeakHashMap<Resources, BridgeContext> sContexts = new WeakHashMap<>();
// TODO: This cache is cleared every time a render session is disposed. Look into making this
// more long lived.
private static LruCache<String, Drawable.ConstantState> sDrawableCache = new LruCache<>(50);
public static Resources initSystem(@NonNull BridgeContext context,
@NonNull AssetManager assets,
@NonNull DisplayMetrics metrics,
@NonNull Configuration config,
@NonNull LayoutlibCallback layoutlibCallback) {
assert Resources.mSystem == null :
"Resources_Delegate.initSystem called twice before disposeSystem was called";
Resources resources = new Resources(Resources_Delegate.class.getClassLoader());
resources.setImpl(new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()));
sContexts.put(resources, Objects.requireNonNull(context));
sLayoutlibCallbacks.put(resources, Objects.requireNonNull(layoutlibCallback));
return Resources.mSystem = resources;
}
/** Returns the {@link BridgeContext} associated to the given {@link Resources} */
@VisibleForTesting
@NonNull
public static BridgeContext getContext(@NonNull Resources resources) {
assert sContexts.containsKey(resources) :
"Resources_Delegate.getContext called before initSystem";
return sContexts.get(resources);
}
/** Returns the {@link LayoutlibCallback} associated to the given {@link Resources} */
@VisibleForTesting
@NonNull
public static LayoutlibCallback getLayoutlibCallback(@NonNull Resources resources) {
assert sLayoutlibCallbacks.containsKey(resources) :
"Resources_Delegate.getLayoutlibCallback called before initSystem";
return sLayoutlibCallbacks.get(resources);
}
/**
* Disposes the static {@link Resources#mSystem} to make sure we don't leave objects around that
* would prevent us from unloading the library.
*/
public static void disposeSystem() {
sDrawableCache.evictAll();
sContexts.clear();
sLayoutlibCallbacks.clear();
Resources.mSystem = null;
}
public static BridgeTypedArray newTypeArray(Resources resources, int numEntries) {
return new BridgeTypedArray(resources, getContext(resources), numEntries);
}
private static ResourceReference getResourceInfo(Resources resources, int id) {
// first get the String related to this id in the framework
ResourceReference resourceInfo = Bridge.resolveResourceId(id);
assert Resources.mSystem != null : "Resources_Delegate.initSystem wasn't called";
// Set the layoutlib callback and context for resources
if (resources != Resources.mSystem &&
(!sContexts.containsKey(resources) || !sLayoutlibCallbacks.containsKey(resources))) {
sLayoutlibCallbacks.put(resources, getLayoutlibCallback(Resources.mSystem));
sContexts.put(resources, getContext(Resources.mSystem));
}
if (resourceInfo == null) {
// Didn't find a match in the framework? Look in the project.
resourceInfo = getLayoutlibCallback(resources).resolveResourceId(id);
}
return resourceInfo;
}
private static Pair<String, ResourceValue> getResourceValue(Resources resources, int id) {
ResourceReference resourceInfo = getResourceInfo(resources, id);
if (resourceInfo != null) {
String attributeName = resourceInfo.getName();
RenderResources renderResources = getContext(resources).getRenderResources();
ResourceValue value = renderResources.getResolvedResource(resourceInfo);
if (value == null) {
// Unable to resolve the attribute, just leave the unresolved value.
value = new ResourceValueImpl(resourceInfo.getNamespace(),
resourceInfo.getResourceType(), attributeName, attributeName);
}
return Pair.of(attributeName, value);
}
return null;
}
@LayoutlibDelegate
static Drawable getDrawable(Resources resources, int id) {
return getDrawable(resources, id, null);
}
@LayoutlibDelegate
static Drawable getDrawable(Resources resources, int id, Theme theme) {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
String key = value.getSecond().getValue();
Drawable.ConstantState constantState = key != null ? sDrawableCache.get(key) : null;
Drawable drawable;
if (constantState != null) {
drawable = constantState.newDrawable(resources, theme);
} else {
drawable =
ResourceHelper.getDrawable(value.getSecond(), getContext(resources), theme);
if (key != null) {
sDrawableCache.put(key, drawable.getConstantState());
}
}
return drawable;
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static int getColor(Resources resources, int id) {
return getColor(resources, id, null);
}
@LayoutlibDelegate
static int getColor(Resources resources, int id, Theme theme) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resourceValue = value.getSecond();
try {
return ResourceHelper.getColor(resourceValue.getValue());
} catch (NumberFormatException e) {
// Check if the value passed is a file. If it is, mostly likely, user is referencing
// a color state list from a place where they should reference only a pure color.
AssetRepository repository = getAssetRepository(resources);
String message;
if (repository.isFileResource(resourceValue.getValue())) {
String resource = (resourceValue.isFramework() ? "@android:" : "@") + "color/"
+ resourceValue.getName();
message = "Hexadecimal color expected, found Color State List for " + resource;
} else {
message = e.getMessage();
}
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, message, e, null);
return 0;
}
}
// Suppress possible NPE. getColorStateList will never return null, it will instead
// throw an exception, but intelliJ can't figure that out
//noinspection ConstantConditions
return getColorStateList(resources, id, theme).getDefaultColor();
}
@LayoutlibDelegate
static ColorStateList getColorStateList(Resources resources, int id) throws NotFoundException {
return getColorStateList(resources, id, null);
}
@LayoutlibDelegate
static ColorStateList getColorStateList(Resources resources, int id, Theme theme)
throws NotFoundException {
Pair<String, ResourceValue> resValue = getResourceValue(resources, id);
if (resValue != null) {
ColorStateList stateList = ResourceHelper.getColorStateList(resValue.getSecond(),
getContext(resources), theme);
if (stateList != null) {
return stateList;
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static CharSequence getText(Resources resources, int id, CharSequence def) {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
return v;
}
}
}
return def;
}
@LayoutlibDelegate
static CharSequence getText(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
return v;
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static CharSequence[] getTextArray(Resources resources, int id) throws NotFoundException {
ResourceValue resValue = getArrayResourceValue(resources, id);
if (resValue == null) {
// Error already logged by getArrayResourceValue.
return new CharSequence[0];
}
if (resValue instanceof ArrayResourceValue) {
ArrayResourceValue arrayValue = (ArrayResourceValue) resValue;
return resolveValues(resources, arrayValue);
}
RenderResources renderResources = getContext(resources).getRenderResources();
return new CharSequence[] { renderResources.resolveResValue(resValue).getValue() };
}
@LayoutlibDelegate
static String[] getStringArray(Resources resources, int id) throws NotFoundException {
ResourceValue resValue = getArrayResourceValue(resources, id);
if (resValue == null) {
// Error already logged by getArrayResourceValue.
return new String[0];
}
if (resValue instanceof ArrayResourceValue) {
ArrayResourceValue arv = (ArrayResourceValue) resValue;
return resolveValues(resources, arv);
}
return new String[] { resolveReference(resources, resValue) };
}
/**
* Resolves each element in resValue and returns an array of resolved values. The returned array
* may contain nulls.
*/
@NonNull
static String[] resolveValues(@NonNull Resources resources,
@NonNull ArrayResourceValue resValue) {
String[] result = new String[resValue.getElementCount()];
for (int i = 0; i < resValue.getElementCount(); i++) {
String value = resValue.getElement(i);
result[i] = resolveReference(resources, value,
resValue.getNamespace(), resValue.getNamespaceResolver());
}
return result;
}
@LayoutlibDelegate
static int[] getIntArray(Resources resources, int id) throws NotFoundException {
ResourceValue rv = getArrayResourceValue(resources, id);
if (rv == null) {
// Error already logged by getArrayResourceValue.
return new int[0];
}
if (rv instanceof ArrayResourceValue) {
ArrayResourceValue resValue = (ArrayResourceValue) rv;
int n = resValue.getElementCount();
int[] values = new int[n];
for (int i = 0; i < n; i++) {
String element = resolveReference(resources, resValue.getElement(i),
resValue.getNamespace(), resValue.getNamespaceResolver());
if (element != null) {
try {
if (element.startsWith("#")) {
// This integer represents a color (starts with #).
values[i] = Color.parseColor(element);
} else {
values[i] = getInt(element);
}
} catch (NumberFormatException e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: \"" + element +
"\"", null);
} catch (IllegalArgumentException e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains wrong color format: \"" + element +
"\"", null);
}
} else {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: \"" +
resValue.getElement(i) + "\"", null);
}
}
return values;
}
// This is an older IDE that can only give us the first element of the array.
String firstValue = resolveReference(resources, rv);
if (firstValue != null) {
try {
return new int[]{getInt(firstValue)};
} catch (NumberFormatException e) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: \"" + firstValue + "\"",
null);
return new int[1];
}
} else {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT,
"Integer resource array contains non-integer value: \"" +
rv.getValue() + "\"", null);
return new int[1];
}
}
/**
* Try to find the ArrayResourceValue for the given id.
* <p/>
* If the ResourceValue found is not of type {@link ResourceType#ARRAY}, the method logs an
* error and return null. However, if the ResourceValue found has type {@code
* ResourceType.ARRAY}, but the value is not an instance of {@link ArrayResourceValue}, the
* method returns the ResourceValue. This happens on older versions of the IDE, which did not
* parse the array resources properly.
* <p/>
*
* @throws NotFoundException if no resource if found
*/
@Nullable
private static ResourceValue getArrayResourceValue(Resources resources, int id)
throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id);
if (v != null) {
ResourceValue resValue = v.getSecond();
assert resValue != null;
if (resValue != null) {
final ResourceType type = resValue.getResourceType();
if (type != ResourceType.ARRAY) {
Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE,
String.format(
"Resource with id 0x%1$X is not an array resource, but %2$s",
id, type == null ? "null" : type.getDisplayName()),
null);
return null;
}
if (!(resValue instanceof ArrayResourceValue)) {
Bridge.getLog().warning(LayoutLog.TAG_UNSUPPORTED,
"Obtaining resource arrays via getTextArray, getStringArray or getIntArray is not fully supported in this version of the IDE.",
null);
}
return resValue;
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@Nullable
private static String resolveReference(@NonNull Resources resources, @Nullable String value,
@NonNull ResourceNamespace contextNamespace,
@NonNull ResourceNamespace.Resolver resolver) {
if (value != null) {
ResourceValue resValue = new UnresolvedResourceValue(value, contextNamespace, resolver);
return resolveReference(resources, resValue);
}
return null;
}
@Nullable
private static String resolveReference(@NonNull Resources resources,
@NonNull ResourceValue value) {
RenderResources renderResources = getContext(resources).getRenderResources();
ResourceValue resolvedValue = renderResources.resolveResValue(value);
return resolvedValue == null ? null : resolvedValue.getValue();
}
@LayoutlibDelegate
static XmlResourceParser getLayout(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id);
if (v != null) {
ResourceValue value = v.getSecond();
try {
BridgeXmlBlockParser parser =
ResourceHelper.getXmlBlockParser(getContext(resources), value);
if (parser != null) {
return parser;
}
} catch (XmlPullParserException e) {
Bridge.getLog().error(LayoutLog.TAG_BROKEN,
"Failed to parse " + value.getValue(), e, null /*data*/);
// we'll return null below.
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id, "layout");
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static XmlResourceParser getAnimation(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id);
if (v != null) {
ResourceValue value = v.getSecond();
try {
return ResourceHelper.getXmlBlockParser(getContext(resources), value);
} catch (XmlPullParserException e) {
Bridge.getLog().error(LayoutLog.TAG_BROKEN,
"Failed to parse " + value.getValue(), e, null /*data*/);
// we'll return null below.
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static TypedArray obtainAttributes(Resources resources, AttributeSet set, int[] attrs) {
return getContext(resources).obtainStyledAttributes(set, attrs);
}
@LayoutlibDelegate
static TypedArray obtainAttributes(Resources resources, Resources.Theme theme, AttributeSet
set, int[] attrs) {
return Resources.obtainAttributes_Original(resources, theme, set, attrs);
}
@LayoutlibDelegate
static TypedArray obtainTypedArray(Resources resources, int id) throws NotFoundException {
BridgeContext context = getContext(resources);
ResourceReference reference = context.resolveId(id);
RenderResources renderResources = context.getRenderResources();
ResourceValue value = renderResources.getResolvedResource(reference);
if (!(value instanceof ArrayResourceValue)) {
throw new NotFoundException("Array resource ID #0x" + Integer.toHexString(id));
}
ArrayResourceValue arrayValue = (ArrayResourceValue) value;
int length = arrayValue.getElementCount();
ResourceNamespace namespace = arrayValue.getNamespace();
BridgeTypedArray typedArray = newTypeArray(resources, length);
for (int i = 0; i < length; i++) {
ResourceValue elementValue;
ResourceUrl resourceUrl = ResourceUrl.parse(arrayValue.getElement(i));
if (resourceUrl != null) {
ResourceReference elementRef =
resourceUrl.resolve(namespace, arrayValue.getNamespaceResolver());
elementValue = renderResources.getResolvedResource(elementRef);
} else {
elementValue = new ResourceValueImpl(namespace, ResourceType.STRING, "element" + i,
arrayValue.getElement(i));
}
typedArray.bridgeSetValue(i, elementValue.getName(), namespace, i, elementValue);
}
typedArray.sealArray();
return typedArray;
}
@LayoutlibDelegate
static float getDimension(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
if (v.equals(BridgeConstants.MATCH_PARENT) ||
v.equals(BridgeConstants.FILL_PARENT)) {
return LayoutParams.MATCH_PARENT;
} else if (v.equals(BridgeConstants.WRAP_CONTENT)) {
return LayoutParams.WRAP_CONTENT;
}
TypedValue tmpValue = new TypedValue();
if (ResourceHelper.parseFloatAttribute(
value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
tmpValue.type == TypedValue.TYPE_DIMENSION) {
return tmpValue.getDimension(resources.getDisplayMetrics());
}
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return 0;
}
@LayoutlibDelegate
static int getDimensionPixelOffset(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
TypedValue tmpValue = new TypedValue();
if (ResourceHelper.parseFloatAttribute(
value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
tmpValue.type == TypedValue.TYPE_DIMENSION) {
return TypedValue.complexToDimensionPixelOffset(tmpValue.data,
resources.getDisplayMetrics());
}
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return 0;
}
@LayoutlibDelegate
static int getDimensionPixelSize(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
TypedValue tmpValue = new TypedValue();
if (ResourceHelper.parseFloatAttribute(
value.getFirst(), v, tmpValue, true /*requireUnit*/) &&
tmpValue.type == TypedValue.TYPE_DIMENSION) {
return TypedValue.complexToDimensionPixelSize(tmpValue.data,
resources.getDisplayMetrics());
}
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return 0;
}
@LayoutlibDelegate
static int getInteger(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
assert resValue != null;
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
try {
return getInt(v);
} catch (NumberFormatException e) {
// return exception below
}
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return 0;
}
@LayoutlibDelegate
static float getFloat(Resources resources, int id) {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
try {
return Float.parseFloat(v);
} catch (NumberFormatException ignore) {
}
}
}
}
return 0;
}
@LayoutlibDelegate
static boolean getBoolean(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resValue = value.getSecond();
if (resValue != null) {
String v = resValue.getValue();
if (v != null) {
return Boolean.parseBoolean(v);
}
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return false;
}
@LayoutlibDelegate
static String getResourceEntryName(Resources resources, int resid) throws NotFoundException {
ResourceReference resourceInfo = getResourceInfo(resources, resid);
if (resourceInfo != null) {
return resourceInfo.getName();
}
throwException(resid, null);
return null;
}
@LayoutlibDelegate
static String getResourceName(Resources resources, int resid) throws NotFoundException {
ResourceReference resourceInfo = getResourceInfo(resources, resid);
if (resourceInfo != null) {
String packageName = getPackageName(resourceInfo, resources);
return packageName + ':' + resourceInfo.getResourceType().getName() + '/' +
resourceInfo.getName();
}
throwException(resid, null);
return null;
}
@LayoutlibDelegate
static String getResourcePackageName(Resources resources, int resid) throws NotFoundException {
ResourceReference resourceInfo = getResourceInfo(resources, resid);
if (resourceInfo != null) {
return getPackageName(resourceInfo, resources);
}
throwException(resid, null);
return null;
}
@LayoutlibDelegate
static String getResourceTypeName(Resources resources, int resid) throws NotFoundException {
ResourceReference resourceInfo = getResourceInfo(resources, resid);
if (resourceInfo != null) {
return resourceInfo.getResourceType().getName();
}
throwException(resid, null);
return null;
}
private static String getPackageName(ResourceReference resourceInfo, Resources resources) {
String packageName = resourceInfo.getNamespace().getPackageName();
if (packageName == null) {
packageName = getContext(resources).getPackageName();
if (packageName == null) {
packageName = SdkConstants.APP_PREFIX;
}
}
return packageName;
}
@LayoutlibDelegate
static String getString(Resources resources, int id, Object... formatArgs)
throws NotFoundException {
String s = getString(resources, id);
if (s != null) {
return String.format(s, formatArgs);
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static String getString(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null && value.getSecond().getValue() != null) {
return value.getSecond().getValue();
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static String getQuantityString(Resources resources, int id, int quantity) throws
NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
if (value.getSecond() instanceof PluralsResourceValue) {
PluralsResourceValue pluralsResourceValue = (PluralsResourceValue) value.getSecond();
PluralRules pluralRules = PluralRules.forLocale(resources.getConfiguration().getLocales()
.get(0));
String strValue = pluralsResourceValue.getValue(pluralRules.select(quantity));
if (strValue == null) {
strValue = pluralsResourceValue.getValue(PluralRules.KEYWORD_OTHER);
}
return strValue;
}
else {
return value.getSecond().getValue();
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static String getQuantityString(Resources resources, int id, int quantity, Object... formatArgs)
throws NotFoundException {
String raw = getQuantityString(resources, id, quantity);
return String.format(resources.getConfiguration().getLocales().get(0), raw, formatArgs);
}
@LayoutlibDelegate
static CharSequence getQuantityText(Resources resources, int id, int quantity) throws
NotFoundException {
return getQuantityString(resources, id, quantity);
}
@LayoutlibDelegate
static Typeface getFont(Resources resources, int id) throws
NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
return ResourceHelper.getFont(value.getSecond(), getContext(resources), null);
}
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static Typeface getFont(Resources resources, TypedValue outValue, int id) throws
NotFoundException {
ResourceValue resVal = getResourceValue(resources, id, outValue);
if (resVal != null) {
return ResourceHelper.getFont(resVal, getContext(resources), null);
}
throwException(resources, id);
return null; // This is not used since the method above always throws.
}
@LayoutlibDelegate
static void getValue(Resources resources, int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
getResourceValue(resources, id, outValue);
}
private static ResourceValue getResourceValue(Resources resources, int id, TypedValue outValue)
throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
ResourceValue resVal = value.getSecond();
String v = resVal != null ? resVal.getValue() : null;
if (v != null) {
if (ResourceHelper.parseFloatAttribute(value.getFirst(), v, outValue,
false /*requireUnit*/)) {
return resVal;
}
if (resVal instanceof DensityBasedResourceValue) {
outValue.density =
((DensityBasedResourceValue) resVal).getResourceDensity().getDpiValue();
}
// else it's a string
outValue.type = TypedValue.TYPE_STRING;
outValue.string = v;
return resVal;
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
return null; // This is not used since the method above always throws.
}
@LayoutlibDelegate
static void getValue(Resources resources, String name, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
throw new UnsupportedOperationException();
}
@LayoutlibDelegate
static void getValueForDensity(Resources resources, int id, int density, TypedValue outValue,
boolean resolveRefs) throws NotFoundException {
getValue(resources, id, outValue, resolveRefs);
}
@LayoutlibDelegate
static int getAttributeSetSourceResId(@Nullable AttributeSet set) {
// Not supported in layoutlib
return Resources.ID_NULL;
}
@LayoutlibDelegate
static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id);
if (v != null) {
ResourceValue value = v.getSecond();
try {
return ResourceHelper.getXmlBlockParser(getContext(resources), value);
} catch (XmlPullParserException e) {
Bridge.getLog().error(LayoutLog.TAG_BROKEN,
"Failed to parse " + value.getValue(), e, null /*data*/);
// we'll return null below.
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static XmlResourceParser loadXmlResourceParser(Resources resources, int id,
String type) throws NotFoundException {
return resources.loadXmlResourceParser_Original(id, type);
}
@LayoutlibDelegate
static XmlResourceParser loadXmlResourceParser(Resources resources, String file, int id,
int assetCookie, String type) throws NotFoundException {
// even though we know the XML file to load directly, we still need to resolve the
// id so that we can know if it's a platform or project resource.
// (mPlatformResouceFlag will get the result and will be used later).
Pair<String, ResourceValue> result = getResourceValue(resources, id);
ResourceNamespace layoutNamespace;
if (result != null && result.getSecond() != null) {
layoutNamespace = result.getSecond().getNamespace();
} else {
// We need to pick something, even though the resource system never heard about a layout
// with this numeric id.
layoutNamespace = ResourceNamespace.RES_AUTO;
}
try {
XmlPullParser parser = ParserFactory.create(file);
return new BridgeXmlBlockParser(parser, getContext(resources), layoutNamespace);
} catch (XmlPullParserException e) {
NotFoundException newE = new NotFoundException();
newE.initCause(e);
throw newE;
}
}
@LayoutlibDelegate
static InputStream openRawResource(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> value = getResourceValue(resources, id);
if (value != null) {
String path = value.getSecond().getValue();
if (path != null) {
return openRawResource(resources, path);
}
}
// id was not found or not resolved. Throw a NotFoundException.
throwException(resources, id);
// this is not used since the method above always throws
return null;
}
@LayoutlibDelegate
static InputStream openRawResource(Resources resources, int id, TypedValue value)
throws NotFoundException {
getValue(resources, id, value, true);
String path = value.string.toString();
return openRawResource(resources, path);
}
private static InputStream openRawResource(Resources resources, String path)
throws NotFoundException {
AssetRepository repository = getAssetRepository(resources);
try {
InputStream stream = repository.openNonAsset(0, path, ACCESS_STREAMING);
if (stream == null) {
throw new NotFoundException(path);
}
// If it's a nine-patch return a custom input stream so that
// other methods (mainly bitmap factory) can detect it's a 9-patch
// and actually load it as a 9-patch instead of a normal bitmap.
if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) {
return new NinePatchInputStream(stream);
}
return stream;
} catch (IOException e) {
NotFoundException exception = new NotFoundException();
exception.initCause(e);
throw exception;
}
}
@LayoutlibDelegate
static AssetFileDescriptor openRawResourceFd(Resources resources, int id)
throws NotFoundException {
throw new UnsupportedOperationException();
}
@VisibleForTesting
@Nullable
static ResourceUrl resourceUrlFromName(
@NonNull String name, @Nullable String defType, @Nullable String defPackage) {
int colonIdx = name.indexOf(':');
int slashIdx = name.indexOf('/');
if (colonIdx != -1 && slashIdx != -1) {
// Easy case
return ResourceUrl.parse(PREFIX_RESOURCE_REF + name);
}
if (colonIdx == -1 && slashIdx == -1) {
if (defType == null) {
throw new IllegalArgumentException("name does not define a type an no defType was" +
" passed");
}
// It does not define package or type
return ResourceUrl.parse(
PREFIX_RESOURCE_REF + (defPackage != null ? defPackage + ":" : "") + defType +
"/" + name);
}
if (colonIdx != -1) {
if (defType == null) {
throw new IllegalArgumentException("name does not define a type an no defType was" +
" passed");
}
// We have package but no type
String pkg = name.substring(0, colonIdx);
ResourceType type = ResourceType.getEnum(defType);
return type != null ? ResourceUrl.create(pkg, type, name.substring(colonIdx + 1)) :
null;
}
ResourceType type = ResourceType.getEnum(name.substring(0, slashIdx));
if (type == null) {
return null;
}
// We have type but no package
return ResourceUrl.create(defPackage,
type,
name.substring(slashIdx + 1));
}
@LayoutlibDelegate
static int getIdentifier(Resources resources, String name, String defType, String defPackage) {
if (name == null) {
return 0;
}
ResourceUrl url = resourceUrlFromName(name, defType, defPackage);
if (url != null) {
if (ANDROID_PKG.equals(url.namespace)) {
return Bridge.getResourceId(url.type, url.name);
}
if (getContext(resources).getPackageName().equals(url.namespace)) {
return getLayoutlibCallback(resources).getOrGenerateResourceId(
new ResourceReference(ResourceNamespace.RES_AUTO, url.type, url.name));
}
}
return 0;
}
/**
* Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource
* type.
*
* @param id the id of the resource
* @param expectedType the type of resource that was expected
*
* @throws NotFoundException
*/
private static void throwException(Resources resources, int id, @Nullable String expectedType)
throws NotFoundException {
throwException(id, getResourceInfo(resources, id), expectedType);
}
private static void throwException(Resources resources, int id) throws NotFoundException {
throwException(resources, id, null);
}
private static void throwException(int id, @Nullable ResourceReference resourceInfo) {
throwException(id, resourceInfo, null);
}
private static void throwException(int id, @Nullable ResourceReference resourceInfo,
@Nullable String expectedType) {
String message;
if (resourceInfo != null) {
message = String.format(
"Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.",
resourceInfo.getResourceType(), id, resourceInfo.getName());
} else {
message = String.format("Could not resolve resource value: 0x%1$X.", id);
}
if (expectedType != null) {
message += " Or the resolved value was not of type " + expectedType + " as expected.";
}
throw new NotFoundException(message);
}
private static int getInt(String v) throws NumberFormatException {
int radix = 10;
if (v.startsWith("0x")) {
v = v.substring(2);
radix = 16;
} else if (v.startsWith("0")) {
radix = 8;
}
return Integer.parseInt(v, radix);
}
private static AssetRepository getAssetRepository(Resources resources) {
BridgeContext context = getContext(resources);
BridgeAssetManager assetManager = context.getAssets();
return assetManager.getAssetRepository();
}
}