| /* |
| * 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.DrawableParams; |
| import com.android.ide.common.rendering.api.LayoutLog; |
| import com.android.ide.common.rendering.api.RenderSession; |
| import com.android.ide.common.rendering.api.ResourceNamespace; |
| import com.android.ide.common.rendering.api.ResourceReference; |
| 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.android.RenderParamsFlags; |
| 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.annotations.Nullable; |
| import com.android.tools.layoutlib.create.MethodAdapter; |
| import com.android.tools.layoutlib.create.OverrideMethod; |
| import com.android.util.Pair; |
| |
| import android.animation.PropertyValuesHolder; |
| import android.animation.PropertyValuesHolder_Delegate; |
| import android.content.res.BridgeAssetManager; |
| import android.graphics.Bitmap; |
| import android.graphics.FontFamily_Delegate; |
| import android.graphics.Typeface; |
| import android.graphics.Typeface_Builder_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.EnumMap; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.WeakHashMap; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| import libcore.io.MemoryMappedFile_Delegate; |
| |
| import static android.graphics.Typeface.DEFAULT_FAMILY; |
| import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE; |
| import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; |
| |
| /** |
| * 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 |
| */ |
| @SuppressWarnings("deprecation") |
| private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>(); |
| |
| /** |
| * 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.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 WeakHashMap<>(); |
| private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache = |
| new WeakHashMap<>(); |
| |
| private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = new HashMap<>(); |
| private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache = |
| new HashMap<>(); |
| |
| private static Map<String, Map<String, Integer>> sEnumValueMap; |
| private static Map<String, String> sPlatformProperties; |
| |
| /** |
| * A default log than prints to stdout/stderr. |
| */ |
| private final static LayoutLog sDefaultLog = new LayoutLog() { |
| @Override |
| public void error(String tag, String message, Object viewCookie, Object data) { |
| System.err.println(message); |
| } |
| |
| @Override |
| public void error(String tag, String message, Throwable throwable, Object viewCookie, |
| Object data) { |
| System.err.println(message); |
| } |
| |
| @Override |
| public void warning(String tag, String message, Object viewCookie, Object data) { |
| System.out.println(message); |
| } |
| }; |
| |
| /** |
| * Current log. |
| */ |
| private static LayoutLog sCurrentLog = sDefaultLog; |
| |
| public static boolean sIsTypefaceInitialized; |
| |
| @Override |
| public boolean init(Map<String,String> platformProperties, |
| File fontLocation, |
| String nativeLibPath, |
| String icuDataPath, |
| 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, 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<>(1280); |
| // no break. |
| default: |
| if (fullMap == null) { |
| fullMap = new HashMap<>(); |
| } |
| 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()) { |
| Integer value = (Integer) f.get(null); |
| //noinspection deprecation |
| 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, 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<>(2048); |
| sRevRMap.put(ResourceType.ATTR, revRAttrMap); |
| // There were 2000 elements in this map at M timeframe. |
| Map<String, Integer> revRStyleableMap = new HashMap<>(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, (o1, 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<>(); |
| 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); |
| 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]; |
| //noinspection deprecation |
| sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName)); |
| revRAttrMap.put(attrName, attrValue); |
| } |
| //noinspection deprecation |
| sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name)); |
| revRStyleableMap.put(name, index); |
| } |
| } |
| |
| @Override |
| public boolean dispose() { |
| BridgeAssetManager.clearSystem(); |
| |
| // dispose of the default typeface. |
| if (sIsTypefaceInitialized) { |
| Typeface.sDynamicTypefaceCache.evictAll(); |
| } |
| sProject9PatchCache.clear(); |
| sProjectBitmapCache.clear(); |
| |
| return true; |
| } |
| |
| /** |
| * Starts a layout session by inflating and rendering it. The method returns a |
| * {@link RenderSession} on which further actions can be taken. |
| * <p/> |
| * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE}, |
| * this method will only inflate the layout but will NOT render it. |
| * @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; |
| RenderSessionImpl scene = new RenderSessionImpl(params); |
| try { |
| prepareThread(); |
| lastResult = scene.init(params.getTimeout()); |
| if (lastResult.isSuccess()) { |
| lastResult = scene.inflate(); |
| |
| boolean doNotRenderOnCreate = Boolean.TRUE.equals( |
| params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE)); |
| if (lastResult.isSuccess() && !doNotRenderOnCreate) { |
| 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 = t2.getCause(); |
| } |
| return new BridgeRenderSession(null, |
| ERROR_UNKNOWN.createResult(t2.getMessage(), t)); |
| } |
| } |
| |
| @Override |
| public Result renderDrawable(DrawableParams params) { |
| try { |
| Result lastResult; |
| 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 clearResourceCaches(Object projectKey) { |
| if (projectKey != null) { |
| sProjectBitmapCache.remove(projectKey); |
| sProject9PatchCache.remove(projectKey); |
| } |
| } |
| |
| @Override |
| public void clearAllCaches(Object projectKey) { |
| clearResourceCaches(projectKey); |
| PropertyValuesHolder_Delegate.clearCaches(); |
| } |
| |
| @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 synchronized 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) { |
| synchronized (Looper.class) { |
| // Check if the main looper has been prepared already. |
| if (Looper.getMainLooper() == 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 synchronized 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. |
| * |
| * <p>TODO(namespaces): remove this and just do all id resolution through the callback. |
| */ |
| @Nullable |
| public static ResourceReference resolveResourceId(int value) { |
| Pair<ResourceType, String> pair = sRMap.get(value); |
| if (pair == null) { |
| pair = sDynamicIds.resolveId(value); |
| } |
| |
| if (pair != null) { |
| return new ResourceReference(ResourceNamespace.ANDROID, pair.getFirst(), pair.getSecond()); |
| } |
| return null; |
| } |
| |
| /** |
| * 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 int containing the resource id. |
| */ |
| public static int getResourceId(ResourceType type, String name) { |
| Map<String, Integer> map = sRevRMap.get(type); |
| Integer value = map == null ? null : map.get(name); |
| return value == null ? sDynamicIds.getId(type, name) : value; |
| } |
| |
| /** |
| * Returns the list of possible enums for a given attribute name. |
| */ |
| @Nullable |
| 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.computeIfAbsent(projectKey, k -> new HashMap<>()); |
| |
| map.put(value, new SoftReference<>(bmp)); |
| } else { |
| sFrameworkBitmapCache.put(value, new SoftReference<>(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.computeIfAbsent(projectKey, k -> new HashMap<>()); |
| |
| map.put(value, new SoftReference<>(ninePatch)); |
| } else { |
| sFramework9PatchCache.put(value, new SoftReference<>(ninePatch)); |
| } |
| } |
| |
| @Override |
| public void clearFontCache(String path) { |
| if (sIsTypefaceInitialized) { |
| final String key = |
| Typeface_Builder_Delegate.createAssetUid(BridgeAssetManager.initSystem(), path, |
| 0, null, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, DEFAULT_FAMILY); |
| Typeface.sDynamicTypefaceCache.remove(key); |
| } |
| } |
| } |