/*
* Copyright (C) 2008 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 com.android.layoutlib.bridge;
import com.android.ide.common.rendering.api.Capability;
import com.android.ide.common.rendering.api.DrawableParams;
import com.android.ide.common.rendering.api.Features;
import com.android.ide.common.rendering.api.LayoutLog;
import com.android.ide.common.rendering.api.RenderSession;
import com.android.ide.common.rendering.api.Result;
import com.android.ide.common.rendering.api.Result.Status;
import com.android.ide.common.rendering.api.SessionParams;
import com.android.layoutlib.bridge.impl.RenderDrawable;
import com.android.layoutlib.bridge.impl.RenderSessionImpl;
import com.android.layoutlib.bridge.util.DynamicIdMap;
import com.android.ninepatch.NinePatchChunk;
import com.android.resources.ResourceType;
import com.android.tools.layoutlib.create.MethodAdapter;
import com.android.tools.layoutlib.create.OverrideMethod;
import com.android.util.Pair;
import android.annotation.NonNull;
import android.content.res.BridgeAssetManager;
import android.graphics.Bitmap;
import android.graphics.FontFamily_Delegate;
import android.graphics.Typeface_Delegate;
import android.icu.util.ULocale;
import android.os.Looper;
import android.os.Looper_Accessor;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import java.io.File;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import libcore.io.MemoryMappedFile_Delegate;
import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
import static com.android.ide.common.rendering.api.Result.Status.SUCCESS;
/**
* Main entry point of the LayoutLib Bridge.
* <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
* {@link #createSession(SessionParams)}
*/
public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
public static class StaticMethodNotImplementedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public StaticMethodNotImplementedException(String msg) {
super(msg);
}
}
/**
* Lock to ensure only one rendering/inflating happens at a time.
* This is due to some singleton in the Android framework.
*/
private final static ReentrantLock sLock = new ReentrantLock();
/**
* Maps from id to resource type/name. This is for com.android.internal.R
*/
private final static Map<Integer, Pair<ResourceType, String>> sRMap =
new HashMap<Integer, Pair<ResourceType, String>>();
/**
* Same as sRMap except for int[] instead of int resources. This is for android.R only.
*/
private final static Map<IntArray, String> sRArrayMap = new HashMap<IntArray, String>(384);
/**
* Reverse map compared to sRMap, resource type -> (resource name -> id).
* This is for com.android.internal.R.
*/
private final static Map<ResourceType, Map<String, Integer>> sRevRMap =
new EnumMap<ResourceType, Map<String,Integer>>(ResourceType.class);
// framework resources are defined as 0x01XX#### where XX is the resource type (layout,
// drawable, etc...). Using FF as the type allows for 255 resource types before we get a
// collision which should be fine.
private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
new HashMap<Object, Map<String, SoftReference<Bitmap>>>();
private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
new HashMap<Object, Map<String, SoftReference<NinePatchChunk>>>();
private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache =
new HashMap<String, SoftReference<Bitmap>>();
private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
new HashMap<String, SoftReference<NinePatchChunk>>();
private static Map<String, Map<String, Integer>> sEnumValueMap;
private static Map<String, String> sPlatformProperties;
/**
* int[] wrapper to use as keys in maps.
*/
private final static class IntArray {
private int[] mArray;
private IntArray() {
// do nothing
}
private IntArray(int[] a) {
mArray = a;
}
private void set(int[] a) {
mArray = a;
}
@Override
public int hashCode() {
return Arrays.hashCode(mArray);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
IntArray other = (IntArray) obj;
return Arrays.equals(mArray, other.mArray);
}
}
/** Instance of IntArrayWrapper to be reused in {@link #resolveResourceId(int[])}. */
private final static IntArray sIntArrayWrapper = new IntArray();
/**
* A default log than prints to stdout/stderr.
*/
private final static LayoutLog sDefaultLog = new LayoutLog() {
@Override
public void error(String tag, String message, Object data) {
System.err.println(message);
}
@Override
public void error(String tag, String message, Throwable throwable, Object data) {
System.err.println(message);
}
@Override
public void warning(String tag, String message, Object data) {
System.out.println(message);
}
};
/**
* Current log.
*/
private static LayoutLog sCurrentLog = sDefaultLog;
private static final int LAST_SUPPORTED_FEATURE = Features.RECYCLER_VIEW_ADAPTER;
@Override
public int getApiLevel() {
return com.android.ide.common.rendering.api.Bridge.API_CURRENT;
}
@Override
@Deprecated
public EnumSet<Capability> getCapabilities() {
// The Capability class is deprecated and frozen. All Capabilities enumerated there are
// supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf()
return EnumSet.allOf(Capability.class);
}
@Override
public boolean supports(int feature) {
return feature <= LAST_SUPPORTED_FEATURE;
}
@Override
public boolean init(Map<String,String> platformProperties,
File fontLocation,
Map<String, Map<String, Integer>> enumValueMap,
LayoutLog log) {
sPlatformProperties = platformProperties;
sEnumValueMap = enumValueMap;
BridgeAssetManager.initSystem();
// When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
// on static (native) methods which prints the signature on the console and
// throws an exception.
// This is useful when testing the rendering in ADT to identify static native
// methods that are ignored -- layoutlib_create makes them returns 0/false/null
// which is generally OK yet might be a problem, so this is how you'd find out.
//
// Currently layoutlib_create only overrides static native method.
// Static non-natives are not overridden and thus do not get here.
final String debug = System.getenv("DEBUG_LAYOUT");
if (debug != null && !debug.equals("0") && !debug.equals("false")) {
OverrideMethod.setDefaultListener(new MethodAdapter() {
@Override
public void onInvokeV(String signature, boolean isNative, Object caller) {
sDefaultLog.error(null, "Missing Stub: " + signature +
(isNative ? " (native)" : ""), null /*data*/);
if (debug.equalsIgnoreCase("throw")) {
// Throwing this exception doesn't seem that useful. It breaks
// the layout editor yet doesn't display anything meaningful to the
// user. Having the error in the console is just as useful. We'll
// throw it only if the environment variable is "throw" or "THROW".
throw new StaticMethodNotImplementedException(signature);
}
}
});
}
// load the fonts.
FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath());
MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile());
// now parse com.android.internal.R (and only this one as android.R is a subset of
// the internal version), and put the content in the maps.
try {
Class<?> r = com.android.internal.R.class;
// Parse the styleable class first, since it may contribute to attr values.
parseStyleable();
for (Class<?> inner : r.getDeclaredClasses()) {
if (inner == com.android.internal.R.styleable.class) {
// Already handled the styleable case. Not skipping attr, as there may be attrs
// that are not referenced from styleables.
continue;
}
String resTypeName = inner.getSimpleName();
ResourceType resType = ResourceType.getEnum(resTypeName);
if (resType != null) {
Map<String, Integer> fullMap = null;
switch (resType) {
case ATTR:
fullMap = sRevRMap.get(ResourceType.ATTR);
break;
case STRING:
case STYLE:
// Slightly less than thousand entries in each.
fullMap = new HashMap<String, Integer>(1280);
// no break.
default:
if (fullMap == null) {
fullMap = new HashMap<String, Integer>();
}
sRevRMap.put(resType, fullMap);
}
for (Field f : inner.getDeclaredFields()) {
// only process static final fields. Since the final attribute may have
// been altered by layoutlib_create, we only check static
if (!isValidRField(f)) {
continue;
}
Class<?> type = f.getType();
if (type.isArray()) {
// if the object is an int[] we put it in sRArrayMap using an IntArray
// wrapper that properly implements equals and hashcode for the array
// objects, as required by the map contract.
sRArrayMap.put(new IntArray((int[]) f.get(null)), f.getName());
} else {
Integer value = (Integer) f.get(null);
sRMap.put(value, Pair.of(resType, f.getName()));
fullMap.put(f.getName(), value);
}
}
}
}
} catch (Exception throwable) {
if (log != null) {
log.error(LayoutLog.TAG_BROKEN,
"Failed to load com.android.internal.R from the layout library jar",
throwable, null);
}
return false;
}
return true;
}
/**
* Tests if the field is pubic, static and one of int or int[].
*/
private static boolean isValidRField(Field field) {
int modifiers = field.getModifiers();
boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
Class<?> type = field.getType();
return isAcceptable && type == int.class ||
(type.isArray() && type.getComponentType() == int.class);
}
private static void parseStyleable() throws Exception {
// R.attr doesn't contain all the needed values. There are too many resources in the
// framework for all to be in the R class. Only the ones specified manually in
// res/values/symbols.xml are put in R class. Since, we need to create a map of all attr
// values, we try and find them from the styleables.
// There were 1500 elements in this map at M timeframe.
Map<String, Integer> revRAttrMap = new HashMap<String, Integer>(2048);
sRevRMap.put(ResourceType.ATTR, revRAttrMap);
// There were 2000 elements in this map at M timeframe.
Map<String, Integer> revRStyleableMap = new HashMap<String, Integer>(3072);
sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap);
Class<?> c = com.android.internal.R.styleable.class;
Field[] fields = c.getDeclaredFields();
// Sort the fields to bring all arrays to the beginning, so that indices into the array are
// able to refer back to the arrays (i.e. no forward references).
Arrays.sort(fields, new Comparator<Field>() {
@Override
public int compare(Field o1, Field o2) {
if (o1 == o2) {
return 0;
}
Class<?> t1 = o1.getType();
Class<?> t2 = o2.getType();
if (t1.isArray() && !t2.isArray()) {
return -1;
} else if (t2.isArray() && !t1.isArray()) {
return 1;
}
return o1.getName().compareTo(o2.getName());
}
});
Map<String, int[]> styleables = new HashMap<String, int[]>();
for (Field field : fields) {
if (!isValidRField(field)) {
// Only consider public static fields that are int or int[].
// Don't check the final flag as it may have been modified by layoutlib_create.
continue;
}
String name = field.getName();
if (field.getType().isArray()) {
int[] styleableValue = (int[]) field.get(null);
sRArrayMap.put(new IntArray(styleableValue), name);
styleables.put(name, styleableValue);
continue;
}
// Not an array.
String arrayName = name;
int[] arrayValue = null;
int index;
while ((index = arrayName.lastIndexOf('_')) >= 0) {
// Find the name of the corresponding styleable.
// Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity
// are mapped to LinearLayout_Layout and not to LinearLayout.
arrayName = arrayName.substring(0, index);
arrayValue = styleables.get(arrayName);
if (arrayValue != null) {
break;
}
}
index = (Integer) field.get(null);
if (arrayValue != null) {
String attrName = name.substring(arrayName.length() + 1);
int attrValue = arrayValue[index];
sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName));
revRAttrMap.put(attrName, attrValue);
}
sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name));
revRStyleableMap.put(name, index);
}
}
@Override
public boolean dispose() {
BridgeAssetManager.clearSystem();
// dispose of the default typeface.
Typeface_Delegate.resetDefaults();
return true;
}
/**
* Starts a layout session by inflating and rendering it. The method returns a
* {@link RenderSession} on which further actions can be taken.
*
* @param params the {@link SessionParams} object with all the information necessary to create
* the scene.
* @return a new {@link RenderSession} object that contains the result of the layout.
* @since 5
*/
@Override
public RenderSession createSession(SessionParams params) {
try {
Result lastResult = SUCCESS.createResult();
RenderSessionImpl scene = new RenderSessionImpl(params);
try {
prepareThread();
lastResult = scene.init(params.getTimeout());
if (lastResult.isSuccess()) {
lastResult = scene.inflate();
if (lastResult.isSuccess()) {
lastResult = scene.render(true /*freshRender*/);
}
}
} finally {
scene.release();
cleanupThread();
}
return new BridgeRenderSession(scene, lastResult);
} catch (Throwable t) {
// get the real cause of the exception.
Throwable t2 = t;
while (t2.getCause() != null) {
t2 = t.getCause();
}
return new BridgeRenderSession(null,
ERROR_UNKNOWN.createResult(t2.getMessage(), t));
}
}
@Override
public Result renderDrawable(DrawableParams params) {
try {
Result lastResult = SUCCESS.createResult();
RenderDrawable action = new RenderDrawable(params);
try {
prepareThread();
lastResult = action.init(params.getTimeout());
if (lastResult.isSuccess()) {
lastResult = action.render();
}
} finally {
action.release();
cleanupThread();
}
return lastResult;
} catch (Throwable t) {
// get the real cause of the exception.
Throwable t2 = t;
while (t2.getCause() != null) {
t2 = t.getCause();
}
return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
}
}
@Override
public void clearCaches(Object projectKey) {
if (projectKey != null) {
sProjectBitmapCache.remove(projectKey);
sProject9PatchCache.remove(projectKey);
}
}
@Override
public Result getViewParent(Object viewObject) {
if (viewObject instanceof View) {
return Status.SUCCESS.createResult(((View)viewObject).getParent());
}
throw new IllegalArgumentException("viewObject is not a View");
}
@Override
public Result getViewIndex(Object viewObject) {
if (viewObject instanceof View) {
View view = (View) viewObject;
ViewParent parentView = view.getParent();
if (parentView instanceof ViewGroup) {
Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
}
return Status.SUCCESS.createResult();
}
throw new IllegalArgumentException("viewObject is not a View");
}
@Override
public boolean isRtl(String locale) {
return isLocaleRtl(locale);
}
public static boolean isLocaleRtl(String locale) {
if (locale == null) {
locale = "";
}
ULocale uLocale = new ULocale(locale);
return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
}
/**
* Returns the lock for the bridge
*/
public static ReentrantLock getLock() {
return sLock;
}
/**
* Prepares the current thread for rendering.
*
* Note that while this can be called several time, the first call to {@link #cleanupThread()}
* will do the clean-up, and make the thread unable to do further scene actions.
*/
public static void prepareThread() {
// we need to make sure the Looper has been initialized for this thread.
// this is required for View that creates Handler objects.
if (Looper.myLooper() == null) {
Looper.prepareMainLooper();
}
}
/**
* Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
* <p>
* Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
* call to this will prevent the thread from doing further scene actions
*/
public static void cleanupThread() {
// clean up the looper
Looper_Accessor.cleanupThread();
}
public static LayoutLog getLog() {
return sCurrentLog;
}
public static void setLog(LayoutLog log) {
// check only the thread currently owning the lock can do this.
if (!sLock.isHeldByCurrentThread()) {
throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
}
if (log != null) {
sCurrentLog = log;
} else {
sCurrentLog = sDefaultLog;
}
}
/**
* Returns details of a framework resource from its integer value.
* @param value the integer value
* @return a Pair containing the resource type and name, or null if the id
* does not match any resource.
*/
public static Pair<ResourceType, String> resolveResourceId(int value) {
Pair<ResourceType, String> pair = sRMap.get(value);
if (pair == null) {
pair = sDynamicIds.resolveId(value);
if (pair == null) {
//System.out.println(String.format("Missing id: %1$08X (%1$d)", value));
}
}
return pair;
}
/**
* Returns the name of a framework resource whose value is an int array.
*/
public static String resolveResourceId(int[] array) {
sIntArrayWrapper.set(array);
return sRArrayMap.get(sIntArrayWrapper);
}
/**
* Returns the integer id of a framework resource, from a given resource type and resource name.
* <p/>
* If no resource is found, it creates a dynamic id for the resource.
*
* @param type the type of the resource
* @param name the name of the resource.
*
* @return an {@link Integer} containing the resource id.
*/
@NonNull
public static Integer getResourceId(ResourceType type, String name) {
Map<String, Integer> map = sRevRMap.get(type);
Integer value = null;
if (map != null) {
value = map.get(name);
}
return value == null ? sDynamicIds.getId(type, name) : value;
}
/**
* Returns the list of possible enums for a given attribute name.
*/
public static Map<String, Integer> getEnumValues(String attributeName) {
if (sEnumValueMap != null) {
return sEnumValueMap.get(attributeName);
}
return null;
}
/**
* Returns the platform build properties.
*/
public static Map<String, String> getPlatformProperties() {
return sPlatformProperties;
}
/**
* Returns the bitmap for a specific path, from a specific project cache, or from the
* framework cache.
* @param value the path of the bitmap
* @param projectKey the key of the project, or null to query the framework cache.
* @return the cached Bitmap or null if not found.
*/
public static Bitmap getCachedBitmap(String value, Object projectKey) {
if (projectKey != null) {
Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
if (map != null) {
SoftReference<Bitmap> ref = map.get(value);
if (ref != null) {
return ref.get();
}
}
} else {
SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
if (ref != null) {
return ref.get();
}
}
return null;
}
/**
* Sets a bitmap in a project cache or in the framework cache.
* @param value the path of the bitmap
* @param bmp the Bitmap object
* @param projectKey the key of the project, or null to put the bitmap in the framework cache.
*/
public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
if (projectKey != null) {
Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
if (map == null) {
map = new HashMap<String, SoftReference<Bitmap>>();
sProjectBitmapCache.put(projectKey, map);
}
map.put(value, new SoftReference<Bitmap>(bmp));
} else {
sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp));
}
}
/**
* Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
* framework cache.
* @param value the path of the 9 patch
* @param projectKey the key of the project, or null to query the framework cache.
* @return the cached 9 patch or null if not found.
*/
public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
if (projectKey != null) {
Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
if (map != null) {
SoftReference<NinePatchChunk> ref = map.get(value);
if (ref != null) {
return ref.get();
}
}
} else {
SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
if (ref != null) {
return ref.get();
}
}
return null;
}
/**
* Sets a 9 patch chunk in a project cache or in the framework cache.
* @param value the path of the 9 patch
* @param ninePatch the 9 patch object
* @param projectKey the key of the project, or null to put the bitmap in the framework cache.
*/
public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
if (projectKey != null) {
Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
if (map == null) {
map = new HashMap<String, SoftReference<NinePatchChunk>>();
sProject9PatchCache.put(projectKey, map);
}
map.put(value, new SoftReference<NinePatchChunk>(ninePatch));
} else {
sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch));
}
}
}