blob: 0121e921bb4cd57576971ef99212d88451781527 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge;
18
19import com.android.ide.common.rendering.api.DrawableParams;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.ide.common.rendering.api.RenderSession;
22import com.android.ide.common.rendering.api.ResourceNamespace;
23import com.android.ide.common.rendering.api.ResourceReference;
24import com.android.ide.common.rendering.api.Result;
25import com.android.ide.common.rendering.api.Result.Status;
26import com.android.ide.common.rendering.api.SessionParams;
27import com.android.layoutlib.bridge.android.RenderParamsFlags;
28import com.android.layoutlib.bridge.impl.RenderDrawable;
29import com.android.layoutlib.bridge.impl.RenderSessionImpl;
30import com.android.layoutlib.bridge.util.DynamicIdMap;
31import com.android.ninepatch.NinePatchChunk;
32import com.android.resources.ResourceType;
33import com.android.tools.layoutlib.annotations.Nullable;
34import com.android.tools.layoutlib.create.MethodAdapter;
35import com.android.tools.layoutlib.create.OverrideMethod;
36import com.android.util.Pair;
37
38import android.animation.PropertyValuesHolder;
39import android.animation.PropertyValuesHolder_Delegate;
40import android.content.res.BridgeAssetManager;
41import android.graphics.Bitmap;
42import android.graphics.FontFamily_Delegate;
43import android.graphics.Typeface;
44import android.graphics.Typeface_Builder_Delegate;
45import android.graphics.Typeface_Delegate;
46import android.icu.util.ULocale;
47import android.os.Looper;
48import android.os.Looper_Accessor;
49import android.view.View;
50import android.view.ViewGroup;
51import android.view.ViewParent;
52
53import java.io.File;
54import java.lang.ref.SoftReference;
55import java.lang.reflect.Field;
56import java.lang.reflect.Modifier;
57import java.util.Arrays;
58import java.util.EnumMap;
59import java.util.EnumSet;
60import java.util.HashMap;
61import java.util.Map;
62import java.util.WeakHashMap;
63import java.util.concurrent.locks.ReentrantLock;
64
65import libcore.io.MemoryMappedFile_Delegate;
66
67import static android.graphics.Typeface.DEFAULT_FAMILY;
68import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE;
69import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN;
70
71/**
72 * Main entry point of the LayoutLib Bridge.
73 * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call
74 * {@link #createSession(SessionParams)}
75 */
76public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
77
78 private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
79
80 public static class StaticMethodNotImplementedException extends RuntimeException {
81 private static final long serialVersionUID = 1L;
82
83 public StaticMethodNotImplementedException(String msg) {
84 super(msg);
85 }
86 }
87
88 /**
89 * Lock to ensure only one rendering/inflating happens at a time.
90 * This is due to some singleton in the Android framework.
91 */
92 private final static ReentrantLock sLock = new ReentrantLock();
93
94 /**
95 * Maps from id to resource type/name. This is for com.android.internal.R
96 */
97 @SuppressWarnings("deprecation")
98 private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>();
99
100 /**
101 * Reverse map compared to sRMap, resource type -> (resource name -> id).
102 * This is for com.android.internal.R.
103 */
104 private final static Map<ResourceType, Map<String, Integer>> sRevRMap = new EnumMap<>(ResourceType.class);
105
106 // framework resources are defined as 0x01XX#### where XX is the resource type (layout,
107 // drawable, etc...). Using FF as the type allows for 255 resource types before we get a
108 // collision which should be fine.
109 private final static int DYNAMIC_ID_SEED_START = 0x01ff0000;
110 private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START);
111
112 private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache =
113 new WeakHashMap<>();
114 private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache =
115 new WeakHashMap<>();
116
117 private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = new HashMap<>();
118 private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache =
119 new HashMap<>();
120
121 private static Map<String, Map<String, Integer>> sEnumValueMap;
122 private static Map<String, String> sPlatformProperties;
123
124 /**
125 * A default log than prints to stdout/stderr.
126 */
127 private final static LayoutLog sDefaultLog = new LayoutLog() {
128 @Override
129 public void error(String tag, String message, Object viewCookie, Object data) {
130 System.err.println(message);
131 }
132
133 @Override
134 public void error(String tag, String message, Throwable throwable, Object viewCookie,
135 Object data) {
136 System.err.println(message);
137 }
138
139 @Override
140 public void warning(String tag, String message, Object viewCookie, Object data) {
141 System.out.println(message);
142 }
143 };
144
145 /**
146 * Current log.
147 */
148 private static LayoutLog sCurrentLog = sDefaultLog;
149
150 public static boolean sIsTypefaceInitialized;
151
152 @Override
153 public boolean init(Map<String,String> platformProperties,
154 File fontLocation,
155 String nativeLibPath,
156 String icuDataPath,
157 Map<String, Map<String, Integer>> enumValueMap,
158 LayoutLog log) {
159 sPlatformProperties = platformProperties;
160 sEnumValueMap = enumValueMap;
161
162 BridgeAssetManager.initSystem();
163
164 // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener
165 // on static (native) methods which prints the signature on the console and
166 // throws an exception.
167 // This is useful when testing the rendering in ADT to identify static native
168 // methods that are ignored -- layoutlib_create makes them returns 0/false/null
169 // which is generally OK yet might be a problem, so this is how you'd find out.
170 //
171 // Currently layoutlib_create only overrides static native method.
172 // Static non-natives are not overridden and thus do not get here.
173 final String debug = System.getenv("DEBUG_LAYOUT");
174 if (debug != null && !debug.equals("0") && !debug.equals("false")) {
175
176 OverrideMethod.setDefaultListener(new MethodAdapter() {
177 @Override
178 public void onInvokeV(String signature, boolean isNative, Object caller) {
179 sDefaultLog.error(null, "Missing Stub: " + signature +
180 (isNative ? " (native)" : ""), null, null /*data*/);
181
182 if (debug.equalsIgnoreCase("throw")) {
183 // Throwing this exception doesn't seem that useful. It breaks
184 // the layout editor yet doesn't display anything meaningful to the
185 // user. Having the error in the console is just as useful. We'll
186 // throw it only if the environment variable is "throw" or "THROW".
187 throw new StaticMethodNotImplementedException(signature);
188 }
189 }
190 });
191 }
192
193 // load the fonts.
194 FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath());
195 MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile());
196
197 // now parse com.android.internal.R (and only this one as android.R is a subset of
198 // the internal version), and put the content in the maps.
199 try {
200 Class<?> r = com.android.internal.R.class;
201 // Parse the styleable class first, since it may contribute to attr values.
202 parseStyleable();
203
204 for (Class<?> inner : r.getDeclaredClasses()) {
205 if (inner == com.android.internal.R.styleable.class) {
206 // Already handled the styleable case. Not skipping attr, as there may be attrs
207 // that are not referenced from styleables.
208 continue;
209 }
210 String resTypeName = inner.getSimpleName();
211 ResourceType resType = ResourceType.getEnum(resTypeName);
212 if (resType != null) {
213 Map<String, Integer> fullMap = null;
214 switch (resType) {
215 case ATTR:
216 fullMap = sRevRMap.get(ResourceType.ATTR);
217 break;
218 case STRING:
219 case STYLE:
220 // Slightly less than thousand entries in each.
221 fullMap = new HashMap<>(1280);
222 // no break.
223 default:
224 if (fullMap == null) {
225 fullMap = new HashMap<>();
226 }
227 sRevRMap.put(resType, fullMap);
228 }
229
230 for (Field f : inner.getDeclaredFields()) {
231 // only process static final fields. Since the final attribute may have
232 // been altered by layoutlib_create, we only check static
233 if (!isValidRField(f)) {
234 continue;
235 }
236 Class<?> type = f.getType();
237 if (!type.isArray()) {
238 Integer value = (Integer) f.get(null);
239 //noinspection deprecation
240 sRMap.put(value, Pair.of(resType, f.getName()));
241 fullMap.put(f.getName(), value);
242 }
243 }
244 }
245 }
246 } catch (Exception throwable) {
247 if (log != null) {
248 log.error(LayoutLog.TAG_BROKEN,
249 "Failed to load com.android.internal.R from the layout library jar",
250 throwable, null, null);
251 }
252 return false;
253 }
254
255 return true;
256 }
257
258 /**
259 * Tests if the field is pubic, static and one of int or int[].
260 */
261 private static boolean isValidRField(Field field) {
262 int modifiers = field.getModifiers();
263 boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
264 Class<?> type = field.getType();
265 return isAcceptable && type == int.class ||
266 (type.isArray() && type.getComponentType() == int.class);
267
268 }
269
270 private static void parseStyleable() throws Exception {
271 // R.attr doesn't contain all the needed values. There are too many resources in the
272 // framework for all to be in the R class. Only the ones specified manually in
273 // res/values/symbols.xml are put in R class. Since, we need to create a map of all attr
274 // values, we try and find them from the styleables.
275
276 // There were 1500 elements in this map at M timeframe.
277 Map<String, Integer> revRAttrMap = new HashMap<>(2048);
278 sRevRMap.put(ResourceType.ATTR, revRAttrMap);
279 // There were 2000 elements in this map at M timeframe.
280 Map<String, Integer> revRStyleableMap = new HashMap<>(3072);
281 sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap);
282 Class<?> c = com.android.internal.R.styleable.class;
283 Field[] fields = c.getDeclaredFields();
284 // Sort the fields to bring all arrays to the beginning, so that indices into the array are
285 // able to refer back to the arrays (i.e. no forward references).
286 Arrays.sort(fields, (o1, o2) -> {
287 if (o1 == o2) {
288 return 0;
289 }
290 Class<?> t1 = o1.getType();
291 Class<?> t2 = o2.getType();
292 if (t1.isArray() && !t2.isArray()) {
293 return -1;
294 } else if (t2.isArray() && !t1.isArray()) {
295 return 1;
296 }
297 return o1.getName().compareTo(o2.getName());
298 });
299 Map<String, int[]> styleables = new HashMap<>();
300 for (Field field : fields) {
301 if (!isValidRField(field)) {
302 // Only consider public static fields that are int or int[].
303 // Don't check the final flag as it may have been modified by layoutlib_create.
304 continue;
305 }
306 String name = field.getName();
307 if (field.getType().isArray()) {
308 int[] styleableValue = (int[]) field.get(null);
309 styleables.put(name, styleableValue);
310 continue;
311 }
312 // Not an array.
313 String arrayName = name;
314 int[] arrayValue = null;
315 int index;
316 while ((index = arrayName.lastIndexOf('_')) >= 0) {
317 // Find the name of the corresponding styleable.
318 // Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity
319 // are mapped to LinearLayout_Layout and not to LinearLayout.
320 arrayName = arrayName.substring(0, index);
321 arrayValue = styleables.get(arrayName);
322 if (arrayValue != null) {
323 break;
324 }
325 }
326 index = (Integer) field.get(null);
327 if (arrayValue != null) {
328 String attrName = name.substring(arrayName.length() + 1);
329 int attrValue = arrayValue[index];
330 //noinspection deprecation
331 sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName));
332 revRAttrMap.put(attrName, attrValue);
333 }
334 //noinspection deprecation
335 sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name));
336 revRStyleableMap.put(name, index);
337 }
338 }
339
340 @Override
341 public boolean dispose() {
342 BridgeAssetManager.clearSystem();
343
344 // dispose of the default typeface.
345 if (sIsTypefaceInitialized) {
346 Typeface.sDynamicTypefaceCache.evictAll();
347 }
348 sProject9PatchCache.clear();
349 sProjectBitmapCache.clear();
350
351 return true;
352 }
353
354 /**
355 * Starts a layout session by inflating and rendering it. The method returns a
356 * {@link RenderSession} on which further actions can be taken.
357 * <p/>
358 * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE},
359 * this method will only inflate the layout but will NOT render it.
360 * @param params the {@link SessionParams} object with all the information necessary to create
361 * the scene.
362 * @return a new {@link RenderSession} object that contains the result of the layout.
363 * @since 5
364 */
365 @Override
366 public RenderSession createSession(SessionParams params) {
367 try {
368 Result lastResult;
369 RenderSessionImpl scene = new RenderSessionImpl(params);
370 try {
371 prepareThread();
372 lastResult = scene.init(params.getTimeout());
373 if (lastResult.isSuccess()) {
374 lastResult = scene.inflate();
375
376 boolean doNotRenderOnCreate = Boolean.TRUE.equals(
377 params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE));
378 if (lastResult.isSuccess() && !doNotRenderOnCreate) {
379 lastResult = scene.render(true /*freshRender*/);
380 }
381 }
382 } finally {
383 scene.release();
384 cleanupThread();
385 }
386
387 return new BridgeRenderSession(scene, lastResult);
388 } catch (Throwable t) {
389 // get the real cause of the exception.
390 Throwable t2 = t;
391 while (t2.getCause() != null) {
392 t2 = t2.getCause();
393 }
394 return new BridgeRenderSession(null,
395 ERROR_UNKNOWN.createResult(t2.getMessage(), t));
396 }
397 }
398
399 @Override
400 public Result renderDrawable(DrawableParams params) {
401 try {
402 Result lastResult;
403 RenderDrawable action = new RenderDrawable(params);
404 try {
405 prepareThread();
406 lastResult = action.init(params.getTimeout());
407 if (lastResult.isSuccess()) {
408 lastResult = action.render();
409 }
410 } finally {
411 action.release();
412 cleanupThread();
413 }
414
415 return lastResult;
416 } catch (Throwable t) {
417 // get the real cause of the exception.
418 Throwable t2 = t;
419 while (t2.getCause() != null) {
420 t2 = t.getCause();
421 }
422 return ERROR_UNKNOWN.createResult(t2.getMessage(), t);
423 }
424 }
425
426 @Override
427 public void clearResourceCaches(Object projectKey) {
428 if (projectKey != null) {
429 sProjectBitmapCache.remove(projectKey);
430 sProject9PatchCache.remove(projectKey);
431 }
432 }
433
434 @Override
435 public void clearAllCaches(Object projectKey) {
436 clearResourceCaches(projectKey);
437 PropertyValuesHolder_Delegate.clearCaches();
438 }
439
440 @Override
441 public Result getViewParent(Object viewObject) {
442 if (viewObject instanceof View) {
443 return Status.SUCCESS.createResult(((View)viewObject).getParent());
444 }
445
446 throw new IllegalArgumentException("viewObject is not a View");
447 }
448
449 @Override
450 public Result getViewIndex(Object viewObject) {
451 if (viewObject instanceof View) {
452 View view = (View) viewObject;
453 ViewParent parentView = view.getParent();
454
455 if (parentView instanceof ViewGroup) {
456 Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view));
457 }
458
459 return Status.SUCCESS.createResult();
460 }
461
462 throw new IllegalArgumentException("viewObject is not a View");
463 }
464
465 @Override
466 public boolean isRtl(String locale) {
467 return isLocaleRtl(locale);
468 }
469
470 public static boolean isLocaleRtl(String locale) {
471 if (locale == null) {
472 locale = "";
473 }
474 ULocale uLocale = new ULocale(locale);
475 return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL);
476 }
477
478 /**
479 * Returns the lock for the bridge
480 */
481 public static ReentrantLock getLock() {
482 return sLock;
483 }
484
485 /**
486 * Prepares the current thread for rendering.
487 *
488 * Note that while this can be called several time, the first call to {@link #cleanupThread()}
489 * will do the clean-up, and make the thread unable to do further scene actions.
490 */
491 public synchronized static void prepareThread() {
492 // We need to make sure the Looper has been initialized for this thread.
493 // This is required for View that creates Handler objects.
494 if (Looper.myLooper() == null) {
495 synchronized (Looper.class) {
496 // Check if the main looper has been prepared already.
497 if (Looper.getMainLooper() == null) {
498 Looper.prepareMainLooper();
499 }
500 }
501 }
502 }
503
504 /**
505 * Cleans up thread-specific data. After this, the thread cannot be used for scene actions.
506 * <p>
507 * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single
508 * call to this will prevent the thread from doing further scene actions
509 */
510 public synchronized static void cleanupThread() {
511 // clean up the looper
512 Looper_Accessor.cleanupThread();
513 }
514
515 public static LayoutLog getLog() {
516 return sCurrentLog;
517 }
518
519 public static void setLog(LayoutLog log) {
520 // check only the thread currently owning the lock can do this.
521 if (!sLock.isHeldByCurrentThread()) {
522 throw new IllegalStateException("scene must be acquired first. see #acquire(long)");
523 }
524
525 if (log != null) {
526 sCurrentLog = log;
527 } else {
528 sCurrentLog = sDefaultLog;
529 }
530 }
531
532 /**
533 * Returns details of a framework resource from its integer value.
534 *
535 * <p>TODO(namespaces): remove this and just do all id resolution through the callback.
536 */
537 @Nullable
538 public static ResourceReference resolveResourceId(int value) {
539 Pair<ResourceType, String> pair = sRMap.get(value);
540 if (pair == null) {
541 pair = sDynamicIds.resolveId(value);
542 }
543
544 if (pair != null) {
545 return new ResourceReference(ResourceNamespace.ANDROID, pair.getFirst(), pair.getSecond());
546 }
547 return null;
548 }
549
550 /**
551 * Returns the integer id of a framework resource, from a given resource type and resource name.
552 * <p/>
553 * If no resource is found, it creates a dynamic id for the resource.
554 *
555 * @param type the type of the resource
556 * @param name the name of the resource.
557 * @return an int containing the resource id.
558 */
559 public static int getResourceId(ResourceType type, String name) {
560 Map<String, Integer> map = sRevRMap.get(type);
561 Integer value = map == null ? null : map.get(name);
562 return value == null ? sDynamicIds.getId(type, name) : value;
563 }
564
565 /**
566 * Returns the list of possible enums for a given attribute name.
567 */
568 @Nullable
569 public static Map<String, Integer> getEnumValues(String attributeName) {
570 if (sEnumValueMap != null) {
571 return sEnumValueMap.get(attributeName);
572 }
573
574 return null;
575 }
576
577 /**
578 * Returns the platform build properties.
579 */
580 public static Map<String, String> getPlatformProperties() {
581 return sPlatformProperties;
582 }
583
584 /**
585 * Returns the bitmap for a specific path, from a specific project cache, or from the
586 * framework cache.
587 * @param value the path of the bitmap
588 * @param projectKey the key of the project, or null to query the framework cache.
589 * @return the cached Bitmap or null if not found.
590 */
591 public static Bitmap getCachedBitmap(String value, Object projectKey) {
592 if (projectKey != null) {
593 Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey);
594 if (map != null) {
595 SoftReference<Bitmap> ref = map.get(value);
596 if (ref != null) {
597 return ref.get();
598 }
599 }
600 } else {
601 SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value);
602 if (ref != null) {
603 return ref.get();
604 }
605 }
606
607 return null;
608 }
609
610 /**
611 * Sets a bitmap in a project cache or in the framework cache.
612 * @param value the path of the bitmap
613 * @param bmp the Bitmap object
614 * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
615 */
616 public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) {
617 if (projectKey != null) {
618 Map<String, SoftReference<Bitmap>> map =
619 sProjectBitmapCache.computeIfAbsent(projectKey, k -> new HashMap<>());
620
621 map.put(value, new SoftReference<>(bmp));
622 } else {
623 sFrameworkBitmapCache.put(value, new SoftReference<>(bmp));
624 }
625 }
626
627 /**
628 * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the
629 * framework cache.
630 * @param value the path of the 9 patch
631 * @param projectKey the key of the project, or null to query the framework cache.
632 * @return the cached 9 patch or null if not found.
633 */
634 public static NinePatchChunk getCached9Patch(String value, Object projectKey) {
635 if (projectKey != null) {
636 Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey);
637
638 if (map != null) {
639 SoftReference<NinePatchChunk> ref = map.get(value);
640 if (ref != null) {
641 return ref.get();
642 }
643 }
644 } else {
645 SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value);
646 if (ref != null) {
647 return ref.get();
648 }
649 }
650
651 return null;
652 }
653
654 /**
655 * Sets a 9 patch chunk in a project cache or in the framework cache.
656 * @param value the path of the 9 patch
657 * @param ninePatch the 9 patch object
658 * @param projectKey the key of the project, or null to put the bitmap in the framework cache.
659 */
660 public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) {
661 if (projectKey != null) {
662 Map<String, SoftReference<NinePatchChunk>> map =
663 sProject9PatchCache.computeIfAbsent(projectKey, k -> new HashMap<>());
664
665 map.put(value, new SoftReference<>(ninePatch));
666 } else {
667 sFramework9PatchCache.put(value, new SoftReference<>(ninePatch));
668 }
669 }
670
671 @Override
672 public void clearFontCache(String path) {
673 if (sIsTypefaceInitialized) {
674 final String key =
675 Typeface_Builder_Delegate.createAssetUid(BridgeAssetManager.initSystem(), path,
676 0, null, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, DEFAULT_FAMILY);
677 Typeface.sDynamicTypefaceCache.remove(key);
678 }
679 }
680}