1 /*
2  * Copyright (C) 2007 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, softwareViewDebug
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 
17 package android.view;
18 
19 import static com.android.internal.util.Preconditions.checkArgument;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.TestApi;
24 import android.compat.annotation.UnsupportedAppUsage;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.HardwareRenderer;
30 import android.graphics.Picture;
31 import android.graphics.RecordingCanvas;
32 import android.graphics.Rect;
33 import android.graphics.RenderNode;
34 import android.os.Build;
35 import android.os.Debug;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.Message;
39 import android.util.Base64;
40 import android.util.DisplayMetrics;
41 import android.util.Log;
42 import android.util.TypedValue;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import libcore.util.HexEncoding;
47 
48 import java.io.BufferedOutputStream;
49 import java.io.BufferedWriter;
50 import java.io.ByteArrayOutputStream;
51 import java.io.DataOutputStream;
52 import java.io.IOException;
53 import java.io.OutputStream;
54 import java.io.OutputStreamWriter;
55 import java.lang.annotation.Annotation;
56 import java.lang.annotation.ElementType;
57 import java.lang.annotation.Retention;
58 import java.lang.annotation.RetentionPolicy;
59 import java.lang.annotation.Target;
60 import java.lang.reflect.AccessibleObject;
61 import java.lang.reflect.Field;
62 import java.lang.reflect.Member;
63 import java.lang.reflect.Method;
64 import java.nio.BufferUnderflowException;
65 import java.nio.ByteBuffer;
66 import java.nio.charset.StandardCharsets;
67 import java.util.ArrayDeque;
68 import java.util.Arrays;
69 import java.util.HashMap;
70 import java.util.concurrent.Callable;
71 import java.util.concurrent.CountDownLatch;
72 import java.util.concurrent.ExecutionException;
73 import java.util.concurrent.Executor;
74 import java.util.concurrent.FutureTask;
75 import java.util.concurrent.TimeUnit;
76 import java.util.concurrent.TimeoutException;
77 import java.util.concurrent.locks.ReentrantLock;
78 import java.util.function.Function;
79 import java.util.stream.Stream;
80 
81 /**
82  * Various debugging/tracing tools related to {@link View} and the view hierarchy.
83  */
84 public class ViewDebug {
85 
86     private static final String TAG = "ViewDebug";
87 
88     /**
89      * @deprecated This flag is now unused
90      */
91     @Deprecated
92     public static final boolean TRACE_HIERARCHY = false;
93 
94     /**
95      * @deprecated This flag is now unused
96      */
97     @Deprecated
98     public static final boolean TRACE_RECYCLER = false;
99 
100     /**
101      * Enables detailed logging of drag/drop operations.
102      * @hide
103      */
104     public static final boolean DEBUG_DRAG = false;
105 
106     /**
107      * Enables detailed logging of task positioning operations.
108      * @hide
109      */
110     public static final boolean DEBUG_POSITIONING = false;
111 
112     /**
113      * This annotation can be used to mark fields and methods to be dumped by
114      * the view server. Only non-void methods with no arguments can be annotated
115      * by this annotation.
116      */
117     @Target({ ElementType.FIELD, ElementType.METHOD })
118     @Retention(RetentionPolicy.RUNTIME)
119     public @interface ExportedProperty {
120         /**
121          * When resolveId is true, and if the annotated field/method return value
122          * is an int, the value is converted to an Android's resource name.
123          *
124          * @return true if the property's value must be transformed into an Android
125          *         resource name, false otherwise
126          */
resolveId()127         boolean resolveId() default false;
128 
129         /**
130          * A mapping can be defined to map int values to specific strings. For
131          * instance, View.getVisibility() returns 0, 4 or 8. However, these values
132          * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see
133          * these human readable values:
134          *
135          * <pre>
136          * {@literal @}ViewDebug.ExportedProperty(mapping = {
137          *     {@literal @}ViewDebug.IntToString(from = 0, to = "VISIBLE"),
138          *     {@literal @}ViewDebug.IntToString(from = 4, to = "INVISIBLE"),
139          *     {@literal @}ViewDebug.IntToString(from = 8, to = "GONE")
140          * })
141          * public int getVisibility() { ...
142          * <pre>
143          *
144          * @return An array of int to String mappings
145          *
146          * @see android.view.ViewDebug.IntToString
147          */
mapping()148         IntToString[] mapping() default { };
149 
150         /**
151          * A mapping can be defined to map array indices to specific strings.
152          * A mapping can be used to see human readable values for the indices
153          * of an array:
154          *
155          * <pre>
156          * {@literal @}ViewDebug.ExportedProperty(indexMapping = {
157          *     {@literal @}ViewDebug.IntToString(from = 0, to = "INVALID"),
158          *     {@literal @}ViewDebug.IntToString(from = 1, to = "FIRST"),
159          *     {@literal @}ViewDebug.IntToString(from = 2, to = "SECOND")
160          * })
161          * private int[] mElements;
162          * <pre>
163          *
164          * @return An array of int to String mappings
165          *
166          * @see android.view.ViewDebug.IntToString
167          * @see #mapping()
168          */
indexMapping()169         IntToString[] indexMapping() default { };
170 
171         /**
172          * A flags mapping can be defined to map flags encoded in an integer to
173          * specific strings. A mapping can be used to see human readable values
174          * for the flags of an integer:
175          *
176          * <pre>
177          * {@literal @}ViewDebug.ExportedProperty(flagMapping = {
178          *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED,
179          *             name = "ENABLED"),
180          *     {@literal @}ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED,
181          *             name = "DISABLED"),
182          * })
183          * private int mFlags;
184          * <pre>
185          *
186          * A specified String is output when the following is true:
187          *
188          * @return An array of int to String mappings
189          */
flagMapping()190         FlagToString[] flagMapping() default { };
191 
192         /**
193          * When deep export is turned on, this property is not dumped. Instead, the
194          * properties contained in this property are dumped. Each child property
195          * is prefixed with the name of this property.
196          *
197          * @return true if the properties of this property should be dumped
198          *
199          * @see #prefix()
200          */
deepExport()201         boolean deepExport() default false;
202 
203         /**
204          * The prefix to use on child properties when deep export is enabled
205          *
206          * @return a prefix as a String
207          *
208          * @see #deepExport()
209          */
prefix()210         String prefix() default "";
211 
212         /**
213          * Specifies the category the property falls into, such as measurement,
214          * layout, drawing, etc.
215          *
216          * @return the category as String
217          */
category()218         String category() default "";
219 
220         /**
221          * Indicates whether or not to format an {@code int} or {@code byte} value as a hex string.
222          *
223          * @return true if the supported values should be formatted as a hex string.
224          */
formatToHexString()225         boolean formatToHexString() default false;
226 
227         /**
228          * Indicates whether or not the key to value mappings are held in adjacent indices.
229          *
230          * Note: Applies only to fields and methods that return String[].
231          *
232          * @return true if the key to value mappings are held in adjacent indices.
233          */
hasAdjacentMapping()234         boolean hasAdjacentMapping() default false;
235     }
236 
237     /**
238      * Defines a mapping from an int value to a String. Such a mapping can be used
239      * in an @ExportedProperty to provide more meaningful values to the end user.
240      *
241      * @see android.view.ViewDebug.ExportedProperty
242      */
243     @Target({ ElementType.TYPE })
244     @Retention(RetentionPolicy.RUNTIME)
245     public @interface IntToString {
246         /**
247          * The original int value to map to a String.
248          *
249          * @return An arbitrary int value.
250          */
from()251         int from();
252 
253         /**
254          * The String to use in place of the original int value.
255          *
256          * @return An arbitrary non-null String.
257          */
to()258         String to();
259     }
260 
261     /**
262      * Defines a mapping from a flag to a String. Such a mapping can be used
263      * in an @ExportedProperty to provide more meaningful values to the end user.
264      *
265      * @see android.view.ViewDebug.ExportedProperty
266      */
267     @Target({ ElementType.TYPE })
268     @Retention(RetentionPolicy.RUNTIME)
269     public @interface FlagToString {
270         /**
271          * The mask to apply to the original value.
272          *
273          * @return An arbitrary int value.
274          */
mask()275         int mask();
276 
277         /**
278          * The value to compare to the result of:
279          * <code>original value &amp; {@link #mask()}</code>.
280          *
281          * @return An arbitrary value.
282          */
equals()283         int equals();
284 
285         /**
286          * The String to use in place of the original int value.
287          *
288          * @return An arbitrary non-null String.
289          */
name()290         String name();
291 
292         /**
293          * Indicates whether to output the flag when the test is true,
294          * or false. Defaults to true.
295          */
outputIf()296         boolean outputIf() default true;
297     }
298 
299     /**
300      * This annotation can be used to mark fields and methods to be dumped when
301      * the view is captured. Methods with this annotation must have no arguments
302      * and must return a valid type of data.
303      */
304     @Target({ ElementType.FIELD, ElementType.METHOD })
305     @Retention(RetentionPolicy.RUNTIME)
306     public @interface CapturedViewProperty {
307         /**
308          * When retrieveReturn is true, we need to retrieve second level methods
309          * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod()
310          * we will set retrieveReturn = true on the annotation of
311          * myView.getFirstLevelMethod()
312          * @return true if we need the second level methods
313          */
retrieveReturn()314         boolean retrieveReturn() default false;
315     }
316 
317     /**
318      * Allows a View to inject custom children into HierarchyViewer. For example,
319      * WebView uses this to add its internal layer tree as a child to itself
320      * @hide
321      */
322     public interface HierarchyHandler {
323         /**
324          * Dumps custom children to hierarchy viewer.
325          * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int)
326          * for the format
327          *
328          * An empty implementation should simply do nothing
329          *
330          * @param out The output writer
331          * @param level The indentation level
332          */
dumpViewHierarchyWithProperties(BufferedWriter out, int level)333         public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
334 
335         /**
336          * Returns a View to enable grabbing screenshots from custom children
337          * returned in dumpViewHierarchyWithProperties.
338          *
339          * @param className The className of the view to find
340          * @param hashCode The hashCode of the view to find
341          * @return the View to capture from, or null if not found
342          */
findHierarchyView(String className, int hashCode)343         public View findHierarchyView(String className, int hashCode);
344     }
345 
346     private abstract static class PropertyInfo<T extends Annotation,
347             R extends AccessibleObject & Member> {
348 
349         public final R member;
350         public final T property;
351         public final String name;
352         public final Class<?> returnType;
353 
354         public String entrySuffix = "";
355         public String valueSuffix = "";
356 
PropertyInfo(Class<T> property, R member, Class<?> returnType)357         PropertyInfo(Class<T> property, R member, Class<?> returnType) {
358             this.member = member;
359             this.name = member.getName();
360             this.property = member.getAnnotation(property);
361             this.returnType = returnType;
362         }
363 
invoke(Object target)364         public abstract Object invoke(Object target) throws Exception;
365 
forMethod(Method method, Class<T> property)366         static <T extends Annotation> PropertyInfo<T, ?> forMethod(Method method,
367                 Class<T> property) {
368             // Ensure the method return and parameter types can be resolved.
369             try {
370                 if ((method.getReturnType() == Void.class)
371                         || (method.getParameterTypes().length != 0)) {
372                     return null;
373                 }
374             } catch (NoClassDefFoundError e) {
375                 return null;
376             }
377             if (!method.isAnnotationPresent(property)) {
378                 return null;
379             }
380             method.setAccessible(true);
381 
382             PropertyInfo info = new MethodPI(method, property);
383             info.entrySuffix = "()";
384             info.valueSuffix = ";";
385             return info;
386         }
387 
forField(Field field, Class<T> property)388         static <T extends Annotation> PropertyInfo<T, ?> forField(Field field, Class<T> property) {
389             if (!field.isAnnotationPresent(property)) {
390                 return null;
391             }
392             field.setAccessible(true);
393             return new FieldPI<>(field, property);
394         }
395     }
396 
397     private static class MethodPI<T extends Annotation> extends PropertyInfo<T, Method> {
398 
MethodPI(Method method, Class<T> property)399         MethodPI(Method method, Class<T> property) {
400             super(property, method, method.getReturnType());
401         }
402 
403         @Override
invoke(Object target)404         public Object invoke(Object target) throws Exception {
405             return member.invoke(target);
406         }
407     }
408 
409     private static class FieldPI<T extends Annotation> extends PropertyInfo<T, Field> {
410 
FieldPI(Field field, Class<T> property)411         FieldPI(Field field, Class<T> property) {
412             super(property, field, field.getType());
413         }
414 
415         @Override
invoke(Object target)416         public Object invoke(Object target) throws Exception {
417             return member.get(target);
418         }
419     }
420 
421     // Maximum delay in ms after which we stop trying to capture a View's drawing
422     private static final int CAPTURE_TIMEOUT = 6000;
423 
424     private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE";
425     private static final String REMOTE_COMMAND_DUMP = "DUMP";
426     private static final String REMOTE_COMMAND_DUMP_THEME = "DUMP_THEME";
427     /**
428      * Similar to REMOTE_COMMAND_DUMP but uses ViewHierarchyEncoder instead of flat text
429      * @hide
430      */
431     public static final String REMOTE_COMMAND_DUMP_ENCODED = "DUMP_ENCODED";
432     private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE";
433     private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT";
434     private static final String REMOTE_PROFILE = "PROFILE";
435     private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS";
436     private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST";
437     private static final String REMOTE_COMMAND_INVOKE_METHOD = "INVOKE_METHOD";
438 
439     private static HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> sExportProperties;
440     private static HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]>
441             sCapturedViewProperties;
442 
443     /**
444      * @deprecated This enum is now unused
445      */
446     @Deprecated
447     public enum HierarchyTraceType {
448         INVALIDATE,
449         INVALIDATE_CHILD,
450         INVALIDATE_CHILD_IN_PARENT,
451         REQUEST_LAYOUT,
452         ON_LAYOUT,
453         ON_MEASURE,
454         DRAW,
455         BUILD_CACHE
456     }
457 
458     /**
459      * @deprecated This enum is now unused
460      */
461     @Deprecated
462     public enum RecyclerTraceType {
463         NEW_VIEW,
464         BIND_VIEW,
465         RECYCLE_FROM_ACTIVE_HEAP,
466         RECYCLE_FROM_SCRAP_HEAP,
467         MOVE_TO_SCRAP_HEAP,
468         MOVE_FROM_ACTIVE_TO_SCRAP_HEAP
469     }
470 
471     /**
472      * Returns the number of instanciated Views.
473      *
474      * @return The number of Views instanciated in the current process.
475      *
476      * @hide
477      */
478     @UnsupportedAppUsage
getViewInstanceCount()479     public static long getViewInstanceCount() {
480         return Debug.countInstancesOfClass(View.class);
481     }
482 
483     /**
484      * Returns the number of instanciated ViewAncestors.
485      *
486      * @return The number of ViewAncestors instanciated in the current process.
487      *
488      * @hide
489      */
490     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getViewRootImplCount()491     public static long getViewRootImplCount() {
492         return Debug.countInstancesOfClass(ViewRootImpl.class);
493     }
494 
495     /**
496      * @deprecated This method is now unused and invoking it is a no-op
497      */
498     @Deprecated
499     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, RecyclerTraceType type, int... parameters)500     public static void trace(View view, RecyclerTraceType type, int... parameters) {
501     }
502 
503     /**
504      * @deprecated This method is now unused and invoking it is a no-op
505      */
506     @Deprecated
507     @SuppressWarnings("UnusedParameters")
startRecyclerTracing(String prefix, View view)508     public static void startRecyclerTracing(String prefix, View view) {
509     }
510 
511     /**
512      * @deprecated This method is now unused and invoking it is a no-op
513      */
514     @Deprecated
515     @SuppressWarnings("UnusedParameters")
stopRecyclerTracing()516     public static void stopRecyclerTracing() {
517     }
518 
519     /**
520      * @deprecated This method is now unused and invoking it is a no-op
521      */
522     @Deprecated
523     @SuppressWarnings({ "UnusedParameters", "deprecation" })
trace(View view, HierarchyTraceType type)524     public static void trace(View view, HierarchyTraceType type) {
525     }
526 
527     /**
528      * @deprecated This method is now unused and invoking it is a no-op
529      */
530     @Deprecated
531     @SuppressWarnings("UnusedParameters")
startHierarchyTracing(String prefix, View view)532     public static void startHierarchyTracing(String prefix, View view) {
533     }
534 
535     /**
536      * @deprecated This method is now unused and invoking it is a no-op
537      */
538     @Deprecated
stopHierarchyTracing()539     public static void stopHierarchyTracing() {
540     }
541 
542     @UnsupportedAppUsage
dispatchCommand(View view, String command, String parameters, OutputStream clientStream)543     static void dispatchCommand(View view, String command, String parameters,
544             OutputStream clientStream) throws IOException {
545         // Just being cautious...
546         view = view.getRootView();
547 
548         if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) {
549             dump(view, false, true, clientStream);
550         } else if (REMOTE_COMMAND_DUMP_THEME.equalsIgnoreCase(command)) {
551             dumpTheme(view, clientStream);
552         } else if (REMOTE_COMMAND_DUMP_ENCODED.equalsIgnoreCase(command)) {
553             dumpEncoded(view, clientStream);
554         } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) {
555             captureLayers(view, new DataOutputStream(clientStream));
556         } else {
557             final String[] params = parameters.split(" ");
558             if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) {
559                 capture(view, clientStream, params[0]);
560             } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) {
561                 outputDisplayList(view, params[0]);
562             } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) {
563                 invalidate(view, params[0]);
564             } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) {
565                 requestLayout(view, params[0]);
566             } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) {
567                 profile(view, clientStream, params[0]);
568             } else if (REMOTE_COMMAND_INVOKE_METHOD.equals(command)) {
569                 invokeViewMethod(view, clientStream, params);
570             }
571         }
572     }
573 
574     /** @hide */
findView(View root, String parameter)575     public static View findView(View root, String parameter) {
576         // Look by type/hashcode
577         if (parameter.indexOf('@') != -1) {
578             final String[] ids = parameter.split("@");
579             final String className = ids[0];
580             final int hashCode = (int) Long.parseLong(ids[1], 16);
581 
582             View view = root.getRootView();
583             if (view instanceof ViewGroup) {
584                 return findView((ViewGroup) view, className, hashCode);
585             }
586         } else {
587             // Look by id
588             final int id = root.getResources().getIdentifier(parameter, null, null);
589             return root.getRootView().findViewById(id);
590         }
591 
592         return null;
593     }
594 
invalidate(View root, String parameter)595     private static void invalidate(View root, String parameter) {
596         final View view = findView(root, parameter);
597         if (view != null) {
598             view.postInvalidate();
599         }
600     }
601 
requestLayout(View root, String parameter)602     private static void requestLayout(View root, String parameter) {
603         final View view = findView(root, parameter);
604         if (view != null) {
605             root.post(new Runnable() {
606                 public void run() {
607                     view.requestLayout();
608                 }
609             });
610         }
611     }
612 
profile(View root, OutputStream clientStream, String parameter)613     private static void profile(View root, OutputStream clientStream, String parameter)
614             throws IOException {
615 
616         final View view = findView(root, parameter);
617         BufferedWriter out = null;
618         try {
619             out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
620 
621             if (view != null) {
622                 profileViewAndChildren(view, out);
623             } else {
624                 out.write("-1 -1 -1");
625                 out.newLine();
626             }
627             out.write("DONE.");
628             out.newLine();
629         } catch (Exception e) {
630             android.util.Log.w("View", "Problem profiling the view:", e);
631         } finally {
632             if (out != null) {
633                 out.close();
634             }
635         }
636     }
637 
638     /** @hide */
profileViewAndChildren(final View view, BufferedWriter out)639     public static void profileViewAndChildren(final View view, BufferedWriter out)
640             throws IOException {
641         RenderNode node = RenderNode.create("ViewDebug", null);
642         profileViewAndChildren(view, node, out, true);
643     }
644 
profileViewAndChildren(View view, RenderNode node, BufferedWriter out, boolean root)645     private static void profileViewAndChildren(View view, RenderNode node, BufferedWriter out,
646             boolean root) throws IOException {
647         long durationMeasure =
648                 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0)
649                         ? profileViewMeasure(view) : 0;
650         long durationLayout =
651                 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0)
652                         ? profileViewLayout(view) : 0;
653         long durationDraw =
654                 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0)
655                         ? profileViewDraw(view, node) : 0;
656 
657         out.write(String.valueOf(durationMeasure));
658         out.write(' ');
659         out.write(String.valueOf(durationLayout));
660         out.write(' ');
661         out.write(String.valueOf(durationDraw));
662         out.newLine();
663         if (view instanceof ViewGroup) {
664             ViewGroup group = (ViewGroup) view;
665             final int count = group.getChildCount();
666             for (int i = 0; i < count; i++) {
667                 profileViewAndChildren(group.getChildAt(i), node, out, false);
668             }
669         }
670     }
671 
profileViewMeasure(final View view)672     private static long profileViewMeasure(final View view) {
673         return profileViewOperation(view, new ViewOperation() {
674             @Override
675             public void pre() {
676                 forceLayout(view);
677             }
678 
679             private void forceLayout(View view) {
680                 view.forceLayout();
681                 if (view instanceof ViewGroup) {
682                     ViewGroup group = (ViewGroup) view;
683                     final int count = group.getChildCount();
684                     for (int i = 0; i < count; i++) {
685                         forceLayout(group.getChildAt(i));
686                     }
687                 }
688             }
689 
690             @Override
691             public void run() {
692                 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec);
693             }
694         });
695     }
696 
697     private static long profileViewLayout(View view) {
698         return profileViewOperation(view,
699                 () -> view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom));
700     }
701 
702     private static long profileViewDraw(View view, RenderNode node) {
703         DisplayMetrics dm = view.getResources().getDisplayMetrics();
704         if (dm == null) {
705             return 0;
706         }
707 
708         if (view.isHardwareAccelerated()) {
709             RecordingCanvas canvas = node.beginRecording(dm.widthPixels, dm.heightPixels);
710             try {
711                 return profileViewOperation(view, () -> view.draw(canvas));
712             } finally {
713                 node.endRecording();
714             }
715         } else {
716             Bitmap bitmap = Bitmap.createBitmap(
717                     dm, dm.widthPixels, dm.heightPixels, Bitmap.Config.RGB_565);
718             Canvas canvas = new Canvas(bitmap);
719             try {
720                 return profileViewOperation(view, () -> view.draw(canvas));
721             } finally {
722                 canvas.setBitmap(null);
723                 bitmap.recycle();
724             }
725         }
726     }
727 
728     interface ViewOperation {
729         default void pre() {}
730 
731         void run();
732     }
733 
734     private static long profileViewOperation(View view, final ViewOperation operation) {
735         final CountDownLatch latch = new CountDownLatch(1);
736         final long[] duration = new long[1];
737 
738         view.post(() -> {
739             try {
740                 operation.pre();
741                 long start = Debug.threadCpuTimeNanos();
742                 //noinspection unchecked
743                 operation.run();
744                 duration[0] = Debug.threadCpuTimeNanos() - start;
745             } finally {
746                 latch.countDown();
747             }
748         });
749 
750         try {
751             if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) {
752                 Log.w("View", "Could not complete the profiling of the view " + view);
753                 return -1;
754             }
755         } catch (InterruptedException e) {
756             Log.w("View", "Could not complete the profiling of the view " + view);
757             Thread.currentThread().interrupt();
758             return -1;
759         }
760 
761         return duration[0];
762     }
763 
764     /** @hide */
765     public static void captureLayers(View root, final DataOutputStream clientStream)
766             throws IOException {
767 
768         try {
769             Rect outRect = new Rect();
770             root.mAttachInfo.mViewRootImpl.getDisplayFrame(outRect);
771 
772             clientStream.writeInt(outRect.width());
773             clientStream.writeInt(outRect.height());
774 
775             captureViewLayer(root, clientStream, true);
776 
777             clientStream.write(2);
778         } finally {
779             clientStream.close();
780         }
781     }
782 
783     private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible)
784             throws IOException {
785 
786         final boolean localVisible = view.getVisibility() == View.VISIBLE && visible;
787 
788         if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) {
789             final int id = view.getId();
790             String name = view.getClass().getSimpleName();
791             if (id != View.NO_ID) {
792                 name = resolveId(view.getContext(), id).toString();
793             }
794 
795             clientStream.write(1);
796             clientStream.writeUTF(name);
797             clientStream.writeByte(localVisible ? 1 : 0);
798 
799             int[] position = new int[2];
800             // XXX: Should happen on the UI thread
801             view.getLocationInWindow(position);
802 
803             clientStream.writeInt(position[0]);
804             clientStream.writeInt(position[1]);
805             clientStream.flush();
806 
807             Bitmap b = performViewCapture(view, true);
808             if (b != null) {
809                 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() *
810                         b.getHeight() * 2);
811                 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut);
812                 clientStream.writeInt(arrayOut.size());
813                 arrayOut.writeTo(clientStream);
814             }
815             clientStream.flush();
816         }
817 
818         if (view instanceof ViewGroup) {
819             ViewGroup group = (ViewGroup) view;
820             int count = group.getChildCount();
821 
822             for (int i = 0; i < count; i++) {
823                 captureViewLayer(group.getChildAt(i), clientStream, localVisible);
824             }
825         }
826 
827         if (view.mOverlay != null) {
828             ViewGroup overlayContainer = view.getOverlay().mOverlayViewGroup;
829             captureViewLayer(overlayContainer, clientStream, localVisible);
830         }
831     }
832 
833     private static void outputDisplayList(View root, String parameter) throws IOException {
834         final View view = findView(root, parameter);
835         view.getViewRootImpl().outputDisplayList(view);
836     }
837 
838     /** @hide */
839     public static void outputDisplayList(View root, View target) {
840         root.getViewRootImpl().outputDisplayList(target);
841     }
842 
843     private static class PictureCallbackHandler implements AutoCloseable,
844             HardwareRenderer.PictureCapturedCallback, Runnable {
845         private final HardwareRenderer mRenderer;
846         private final Function<Picture, Boolean> mCallback;
847         private final Executor mExecutor;
848         private final ReentrantLock mLock = new ReentrantLock(false);
849         private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
850         private boolean mStopListening;
851         private Thread mRenderThread;
852 
853         private PictureCallbackHandler(HardwareRenderer renderer,
854                 Function<Picture, Boolean> callback, Executor executor) {
855             mRenderer = renderer;
856             mCallback = callback;
857             mExecutor = executor;
858             mRenderer.setPictureCaptureCallback(this);
859         }
860 
861         @Override
862         public void close() {
863             mLock.lock();
864             mStopListening = true;
865             mLock.unlock();
866             mRenderer.setPictureCaptureCallback(null);
867         }
868 
869         @Override
870         public void onPictureCaptured(Picture picture) {
871             mLock.lock();
872             if (mStopListening) {
873                 mLock.unlock();
874                 mRenderer.setPictureCaptureCallback(null);
875                 return;
876             }
877             if (mRenderThread == null) {
878                 mRenderThread = Thread.currentThread();
879             }
880             Picture toDestroy = null;
881             if (mQueue.size() == 3) {
882                 toDestroy = mQueue.removeLast();
883             }
884             mQueue.add(picture);
885             mLock.unlock();
886             if (toDestroy == null) {
887                 mExecutor.execute(this);
888             } else {
889                 toDestroy.close();
890             }
891         }
892 
893         @Override
894         public void run() {
895             mLock.lock();
896             final Picture picture = mQueue.poll();
897             final boolean isStopped = mStopListening;
898             mLock.unlock();
899             if (Thread.currentThread() == mRenderThread) {
900                 close();
901                 throw new IllegalStateException(
902                         "ViewDebug#startRenderingCommandsCapture must be given an executor that "
903                         + "invokes asynchronously");
904             }
905             if (isStopped) {
906                 picture.close();
907                 return;
908             }
909             final boolean keepReceiving = mCallback.apply(picture);
910             if (!keepReceiving) {
911                 close();
912             }
913         }
914     }
915 
916     /**
917      * Begins capturing the entire rendering commands for the view tree referenced by the given
918      * view. The view passed may be any View in the tree as long as it is attached. That is,
919      * {@link View#isAttachedToWindow()} must be true.
920      *
921      * Every time a frame is rendered a Picture will be passed to the given callback via the given
922      * executor. As long as the callback returns 'true' it will continue to receive new frames.
923      * The system will only invoke the callback at a rate that the callback is able to keep up with.
924      * That is, if it takes 48ms for the callback to complete and there is a 60fps animation running
925      * then the callback will only receive 33% of the frames produced.
926      *
927      * This method must be called on the same thread as the View tree.
928      *
929      * @param tree The View tree to capture the rendering commands.
930      * @param callback The callback to invoke on every frame produced. Should return true to
931      *                 continue receiving new frames, false to stop capturing.
932      * @param executor The executor to invoke the callback on. Recommend using a background thread
933      *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
934      *                 exception will be thrown.
935      * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
936      * that the callback may continue to receive another frame or two depending on thread timings.
937      * Returns null if the capture stream cannot be started, such as if there's no
938      * HardwareRenderer for the given view tree.
939      * @hide
940      * @deprecated use {@link #startRenderingCommandsCapture(View, Executor, Callable)} instead.
941      */
942     @TestApi
943     @Nullable
944     @Deprecated
945     public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
946             Function<Picture, Boolean> callback) {
947         final View.AttachInfo attachInfo = tree.mAttachInfo;
948         if (attachInfo == null) {
949             throw new IllegalArgumentException("Given view isn't attached");
950         }
951         if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
952             throw new IllegalStateException("Called on the wrong thread."
953                     + " Must be called on the thread that owns the given View");
954         }
955         final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
956         if (renderer != null) {
957             return new PictureCallbackHandler(renderer, callback, executor);
958         }
959         return null;
960     }
961 
962     private static class StreamingPictureCallbackHandler implements AutoCloseable,
963             HardwareRenderer.PictureCapturedCallback, Runnable {
964         private final HardwareRenderer mRenderer;
965         private final Callable<OutputStream> mCallback;
966         private final Executor mExecutor;
967         private final ReentrantLock mLock = new ReentrantLock(false);
968         private final ArrayDeque<Picture> mQueue = new ArrayDeque<>(3);
969         private boolean mStopListening;
970         private Thread mRenderThread;
971 
972         private StreamingPictureCallbackHandler(HardwareRenderer renderer,
973                 Callable<OutputStream> callback, Executor executor) {
974             mRenderer = renderer;
975             mCallback = callback;
976             mExecutor = executor;
977             mRenderer.setPictureCaptureCallback(this);
978         }
979 
980         @Override
981         public void close() {
982             mLock.lock();
983             mStopListening = true;
984             mLock.unlock();
985             mRenderer.setPictureCaptureCallback(null);
986         }
987 
988         @Override
989         public void onPictureCaptured(Picture picture) {
990             mLock.lock();
991             if (mStopListening) {
992                 mLock.unlock();
993                 mRenderer.setPictureCaptureCallback(null);
994                 return;
995             }
996             if (mRenderThread == null) {
997                 mRenderThread = Thread.currentThread();
998             }
999             boolean needsInvoke = true;
1000             if (mQueue.size() == 3) {
1001                 mQueue.removeLast();
1002                 needsInvoke = false;
1003             }
1004             mQueue.add(picture);
1005             mLock.unlock();
1006 
1007             if (needsInvoke) {
1008                 mExecutor.execute(this);
1009             }
1010         }
1011 
1012         @Override
1013         public void run() {
1014             mLock.lock();
1015             final Picture picture = mQueue.poll();
1016             final boolean isStopped = mStopListening;
1017             mLock.unlock();
1018             if (Thread.currentThread() == mRenderThread) {
1019                 close();
1020                 throw new IllegalStateException(
1021                         "ViewDebug#startRenderingCommandsCapture must be given an executor that "
1022                         + "invokes asynchronously");
1023             }
1024             if (isStopped) {
1025                 return;
1026             }
1027             OutputStream stream = null;
1028             try {
1029                 stream = mCallback.call();
1030             } catch (Exception ex) {
1031                 Log.w("ViewDebug", "Aborting rendering commands capture "
1032                         + "because callback threw exception", ex);
1033             }
1034             if (stream != null) {
1035                 try {
1036                     picture.writeToStream(stream);
1037                     stream.flush();
1038                 } catch (IOException ex) {
1039                     Log.w("ViewDebug", "Aborting rendering commands capture "
1040                             + "due to IOException writing to output stream", ex);
1041                 }
1042             } else {
1043                 close();
1044             }
1045         }
1046     }
1047 
1048     /**
1049      * Begins capturing the entire rendering commands for the view tree referenced by the given
1050      * view. The view passed may be any View in the tree as long as it is attached. That is,
1051      * {@link View#isAttachedToWindow()} must be true.
1052      *
1053      * Every time a frame is rendered the callback will be invoked on the given executor to
1054      * provide an OutputStream to serialize to. As long as the callback returns a valid
1055      * OutputStream the capturing will continue. The system will only invoke the callback at a rate
1056      * that the callback & OutputStream is able to keep up with. That is, if it takes 48ms for the
1057      * callback & serialization to complete and there is a 60fps animation running
1058      * then the callback will only receive 33% of the frames produced.
1059      *
1060      * This method must be called on the same thread as the View tree.
1061      *
1062      * @param tree The View tree to capture the rendering commands.
1063      * @param callback The callback to invoke on every frame produced. Should return an
1064      *                 OutputStream to write the data to. Return null to cancel capture. The
1065      *                 same stream may be returned each time as the serialized data contains
1066      *                 start & end markers. The callback will not be invoked while a previous
1067      *                 serialization is being performed, so if a single continuous stream is being
1068      *                 used it is valid for the callback to write its own metadata to that stream
1069      *                 in response to callback invocation.
1070      * @param executor The executor to invoke the callback on. Recommend using a background thread
1071      *                 to avoid stalling the UI thread. Must be an asynchronous invoke or an
1072      *                 exception will be thrown.
1073      * @return a closeable that can be used to stop capturing. May be invoked on any thread. Note
1074      * that the callback may continue to receive another frame or two depending on thread timings.
1075      * Returns null if the capture stream cannot be started, such as if there's no
1076      * HardwareRenderer for the given view tree.
1077      * @hide
1078      */
1079     @TestApi
1080     @Nullable
1081     @UnsupportedAppUsage // Visible for Studio; least-worst option available
1082     public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
1083             Callable<OutputStream> callback) {
1084         final View.AttachInfo attachInfo = tree.mAttachInfo;
1085         if (attachInfo == null) {
1086             throw new IllegalArgumentException("Given view isn't attached");
1087         }
1088         if (attachInfo.mHandler.getLooper() != Looper.myLooper()) {
1089             throw new IllegalStateException("Called on the wrong thread."
1090                     + " Must be called on the thread that owns the given View");
1091         }
1092         final HardwareRenderer renderer = attachInfo.mThreadedRenderer;
1093         if (renderer != null) {
1094             return new StreamingPictureCallbackHandler(renderer, callback, executor);
1095         }
1096         return null;
1097     }
1098 
1099     private static void capture(View root, final OutputStream clientStream, String parameter)
1100             throws IOException {
1101 
1102         final View captureView = findView(root, parameter);
1103         capture(root, clientStream, captureView);
1104     }
1105 
1106     /** @hide */
1107     public static void capture(View root, final OutputStream clientStream, View captureView)
1108             throws IOException {
1109         Bitmap b = performViewCapture(captureView, false);
1110 
1111         if (b == null) {
1112             Log.w("View", "Failed to create capture bitmap!");
1113             // Send an empty one so that it doesn't get stuck waiting for
1114             // something.
1115             b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(),
1116                     1, 1, Bitmap.Config.ARGB_8888);
1117         }
1118 
1119         BufferedOutputStream out = null;
1120         try {
1121             out = new BufferedOutputStream(clientStream, 32 * 1024);
1122             b.compress(Bitmap.CompressFormat.PNG, 100, out);
1123             out.flush();
1124         } finally {
1125             if (out != null) {
1126                 out.close();
1127             }
1128             b.recycle();
1129         }
1130     }
1131 
1132     private static Bitmap performViewCapture(final View captureView, final boolean skipChildren) {
1133         if (captureView != null) {
1134             final CountDownLatch latch = new CountDownLatch(1);
1135             final Bitmap[] cache = new Bitmap[1];
1136 
1137             captureView.post(() -> {
1138                 try {
1139                     CanvasProvider provider = captureView.isHardwareAccelerated()
1140                             ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
1141                     cache[0] = captureView.createSnapshot(provider, skipChildren);
1142                 } catch (OutOfMemoryError e) {
1143                     Log.w("View", "Out of memory for bitmap");
1144                 } finally {
1145                     latch.countDown();
1146                 }
1147             });
1148 
1149             try {
1150                 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS);
1151                 return cache[0];
1152             } catch (InterruptedException e) {
1153                 Log.w("View", "Could not complete the capture of the view " + captureView);
1154                 Thread.currentThread().interrupt();
1155             }
1156         }
1157 
1158         return null;
1159     }
1160 
1161     /**
1162      * Dumps the view hierarchy starting from the given view.
1163      * @deprecated See {@link #dumpv2(View, ByteArrayOutputStream)} below.
1164      * @hide
1165      */
1166     @Deprecated
1167     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1168     public static void dump(View root, boolean skipChildren, boolean includeProperties,
1169             OutputStream clientStream) throws IOException {
1170         BufferedWriter out = null;
1171         try {
1172             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1173             View view = root.getRootView();
1174             if (view instanceof ViewGroup) {
1175                 ViewGroup group = (ViewGroup) view;
1176                 dumpViewHierarchy(group.getContext(), group, out, 0,
1177                         skipChildren, includeProperties);
1178             }
1179             out.write("DONE.");
1180             out.newLine();
1181         } catch (Exception e) {
1182             android.util.Log.w("View", "Problem dumping the view:", e);
1183         } finally {
1184             if (out != null) {
1185                 out.close();
1186             }
1187         }
1188     }
1189 
1190     /**
1191      * Dumps the view hierarchy starting from the given view.
1192      * Rather than using reflection, it uses View's encode method to obtain all the properties.
1193      * @hide
1194      */
1195     public static void dumpv2(@NonNull final View view, @NonNull ByteArrayOutputStream out)
1196             throws InterruptedException {
1197         final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(out);
1198         final CountDownLatch latch = new CountDownLatch(1);
1199 
1200         view.post(new Runnable() {
1201             @Override
1202             public void run() {
1203                 encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
1204                 encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
1205                 view.encode(encoder);
1206                 latch.countDown();
1207             }
1208         });
1209 
1210         latch.await(2, TimeUnit.SECONDS);
1211         encoder.endStream();
1212     }
1213 
1214     private static void dumpEncoded(@NonNull final View view, @NonNull OutputStream out)
1215             throws IOException {
1216         ByteArrayOutputStream baOut = new ByteArrayOutputStream();
1217 
1218         final ViewHierarchyEncoder encoder = new ViewHierarchyEncoder(baOut);
1219         encoder.setUserPropertiesEnabled(false);
1220         encoder.addProperty("window:left", view.mAttachInfo.mWindowLeft);
1221         encoder.addProperty("window:top", view.mAttachInfo.mWindowTop);
1222         view.encode(encoder);
1223         encoder.endStream();
1224         out.write(baOut.toByteArray());
1225     }
1226 
1227     /**
1228      * Dumps the theme attributes from the given View.
1229      * @hide
1230      */
1231     public static void dumpTheme(View view, OutputStream clientStream) throws IOException {
1232         BufferedWriter out = null;
1233         try {
1234             out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024);
1235             String[] attributes = getStyleAttributesDump(view.getContext().getResources(),
1236                     view.getContext().getTheme());
1237             if (attributes != null) {
1238                 for (int i = 0; i < attributes.length; i += 2) {
1239                     if (attributes[i] != null) {
1240                         out.write(attributes[i] + "\n");
1241                         out.write(attributes[i + 1] + "\n");
1242                     }
1243                 }
1244             }
1245             out.write("DONE.");
1246             out.newLine();
1247         } catch (Exception e) {
1248             android.util.Log.w("View", "Problem dumping View Theme:", e);
1249         } finally {
1250             if (out != null) {
1251                 out.close();
1252             }
1253         }
1254     }
1255 
1256     /**
1257      * Gets the style attributes from the {@link Resources.Theme}. For debugging only.
1258      *
1259      * @param resources Resources to resolve attributes from.
1260      * @param theme Theme to dump.
1261      * @return a String array containing pairs of adjacent Theme attribute data: name followed by
1262      * its value.
1263      *
1264      * @hide
1265      */
1266     private static String[] getStyleAttributesDump(Resources resources, Resources.Theme theme) {
1267         TypedValue outValue = new TypedValue();
1268         String nullString = "null";
1269         int i = 0;
1270         int[] attributes = theme.getAllAttributes();
1271         String[] data = new String[attributes.length * 2];
1272         for (int attributeId : attributes) {
1273             try {
1274                 data[i] = resources.getResourceName(attributeId);
1275                 data[i + 1] = theme.resolveAttribute(attributeId, outValue, true) ?
1276                         outValue.coerceToString().toString() :  nullString;
1277                 i += 2;
1278 
1279                 // attempt to replace reference data with its name
1280                 if (outValue.type == TypedValue.TYPE_REFERENCE) {
1281                     data[i - 1] = resources.getResourceName(outValue.resourceId);
1282                 }
1283             } catch (Resources.NotFoundException e) {
1284                 // ignore resources we can't resolve
1285             }
1286         }
1287         return data;
1288     }
1289 
1290     private static View findView(ViewGroup group, String className, int hashCode) {
1291         if (isRequestedView(group, className, hashCode)) {
1292             return group;
1293         }
1294 
1295         final int count = group.getChildCount();
1296         for (int i = 0; i < count; i++) {
1297             final View view = group.getChildAt(i);
1298             if (view instanceof ViewGroup) {
1299                 final View found = findView((ViewGroup) view, className, hashCode);
1300                 if (found != null) {
1301                     return found;
1302                 }
1303             } else if (isRequestedView(view, className, hashCode)) {
1304                 return view;
1305             }
1306             if (view.mOverlay != null) {
1307                 final View found = findView((ViewGroup) view.mOverlay.mOverlayViewGroup,
1308                         className, hashCode);
1309                 if (found != null) {
1310                     return found;
1311                 }
1312             }
1313             if (view instanceof HierarchyHandler) {
1314                 final View found = ((HierarchyHandler)view)
1315                         .findHierarchyView(className, hashCode);
1316                 if (found != null) {
1317                     return found;
1318                 }
1319             }
1320         }
1321         return null;
1322     }
1323 
1324     private static boolean isRequestedView(View view, String className, int hashCode) {
1325         if (view.hashCode() == hashCode) {
1326             String viewClassName = view.getClass().getName();
1327             if (className.equals("ViewOverlay")) {
1328                 return viewClassName.equals("android.view.ViewOverlay$OverlayViewGroup");
1329             } else {
1330                 return className.equals(viewClassName);
1331             }
1332         }
1333         return false;
1334     }
1335 
1336     private static void dumpViewHierarchy(Context context, ViewGroup group,
1337             BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
1338         cacheExportedProperties(group.getClass());
1339         if (!skipChildren) {
1340             cacheExportedPropertiesForChildren(group);
1341         }
1342         // Try to use the handler provided by the view
1343         Handler handler = group.getHandler();
1344         // Fall back on using the main thread
1345         if (handler == null) {
1346             handler = new Handler(Looper.getMainLooper());
1347         }
1348 
1349         if (handler.getLooper() == Looper.myLooper()) {
1350             dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
1351                     includeProperties);
1352         } else {
1353             FutureTask task = new FutureTask(() ->
1354                     dumpViewHierarchyOnUIThread(context, group, out, level, skipChildren,
1355                             includeProperties), null);
1356             Message msg = Message.obtain(handler, task);
1357             msg.setAsynchronous(true);
1358             handler.sendMessage(msg);
1359             while (true) {
1360                 try {
1361                     task.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
1362                     return;
1363                 } catch (InterruptedException e) {
1364                     // try again
1365                 } catch (ExecutionException | TimeoutException e) {
1366                     // Something unexpected happened.
1367                     throw new RuntimeException(e);
1368                 }
1369             }
1370         }
1371     }
1372 
1373     private static void cacheExportedPropertiesForChildren(ViewGroup group) {
1374         final int count = group.getChildCount();
1375         for (int i = 0; i < count; i++) {
1376             final View view = group.getChildAt(i);
1377             cacheExportedProperties(view.getClass());
1378             if (view instanceof ViewGroup) {
1379                 cacheExportedPropertiesForChildren((ViewGroup) view);
1380             }
1381         }
1382     }
1383 
1384     private static void cacheExportedProperties(Class<?> klass) {
1385         if (sExportProperties != null && sExportProperties.containsKey(klass)) {
1386             return;
1387         }
1388         do {
1389             for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
1390                 if (!info.returnType.isPrimitive() && info.property.deepExport()) {
1391                     cacheExportedProperties(info.returnType);
1392                 }
1393             }
1394             klass = klass.getSuperclass();
1395         } while (klass != Object.class);
1396     }
1397 
1398 
1399     private static void dumpViewHierarchyOnUIThread(Context context, ViewGroup group,
1400             BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) {
1401         if (!dumpView(context, group, out, level, includeProperties)) {
1402             return;
1403         }
1404 
1405         if (skipChildren) {
1406             return;
1407         }
1408 
1409         final int count = group.getChildCount();
1410         for (int i = 0; i < count; i++) {
1411             final View view = group.getChildAt(i);
1412             if (view instanceof ViewGroup) {
1413                 dumpViewHierarchyOnUIThread(context, (ViewGroup) view, out, level + 1,
1414                         skipChildren, includeProperties);
1415             } else {
1416                 dumpView(context, view, out, level + 1, includeProperties);
1417             }
1418             if (view.mOverlay != null) {
1419                 ViewOverlay overlay = view.getOverlay();
1420                 ViewGroup overlayContainer = overlay.mOverlayViewGroup;
1421                 dumpViewHierarchyOnUIThread(context, overlayContainer, out, level + 2,
1422                         skipChildren, includeProperties);
1423             }
1424         }
1425         if (group instanceof HierarchyHandler) {
1426             ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1);
1427         }
1428     }
1429 
1430     private static boolean dumpView(Context context, View view,
1431             BufferedWriter out, int level, boolean includeProperties) {
1432 
1433         try {
1434             for (int i = 0; i < level; i++) {
1435                 out.write(' ');
1436             }
1437             String className = view.getClass().getName();
1438             if (className.equals("android.view.ViewOverlay$OverlayViewGroup")) {
1439                 className = "ViewOverlay";
1440             }
1441             out.write(className);
1442             out.write('@');
1443             out.write(Integer.toHexString(view.hashCode()));
1444             out.write(' ');
1445             if (includeProperties) {
1446                 dumpViewProperties(context, view, out);
1447             }
1448             out.newLine();
1449         } catch (IOException e) {
1450             Log.w("View", "Error while dumping hierarchy tree");
1451             return false;
1452         }
1453         return true;
1454     }
1455 
1456     private static <T extends Annotation> PropertyInfo<T, ?>[] convertToPropertyInfos(
1457             Method[] methods, Field[] fields, Class<T> property) {
1458         return Stream.of(Arrays.stream(methods).map(m -> PropertyInfo.forMethod(m, property)),
1459                 Arrays.stream(fields).map(f -> PropertyInfo.forField(f, property)))
1460                 .flatMap(Function.identity())
1461                 .filter(i -> i != null)
1462                 .toArray(PropertyInfo[]::new);
1463     }
1464 
1465     private static PropertyInfo<ExportedProperty, ?>[] getExportedProperties(Class<?> klass) {
1466         if (sExportProperties == null) {
1467             sExportProperties = new HashMap<>();
1468         }
1469         final HashMap<Class<?>, PropertyInfo<ExportedProperty, ?>[]> map = sExportProperties;
1470         PropertyInfo<ExportedProperty, ?>[] properties = sExportProperties.get(klass);
1471 
1472         if (properties == null) {
1473             properties = convertToPropertyInfos(klass.getDeclaredMethods(),
1474                     klass.getDeclaredFields(), ExportedProperty.class);
1475             map.put(klass, properties);
1476         }
1477         return properties;
1478     }
1479 
1480     private static void dumpViewProperties(Context context, Object view,
1481             BufferedWriter out) throws IOException {
1482 
1483         dumpViewProperties(context, view, out, "");
1484     }
1485 
1486     private static void dumpViewProperties(Context context, Object view,
1487             BufferedWriter out, String prefix) throws IOException {
1488 
1489         if (view == null) {
1490             out.write(prefix + "=4,null ");
1491             return;
1492         }
1493 
1494         Class<?> klass = view.getClass();
1495         do {
1496             writeExportedProperties(context, view, out, klass, prefix);
1497             klass = klass.getSuperclass();
1498         } while (klass != Object.class);
1499     }
1500 
1501     private static String formatIntToHexString(int value) {
1502         return "0x" + Integer.toHexString(value).toUpperCase();
1503     }
1504 
1505     private static void writeExportedProperties(Context context, Object view, BufferedWriter out,
1506             Class<?> klass, String prefix) throws IOException {
1507         for (PropertyInfo<ExportedProperty, ?> info : getExportedProperties(klass)) {
1508             //noinspection EmptyCatchBlock
1509             Object value;
1510             try {
1511                 value = info.invoke(view);
1512             } catch (Exception e) {
1513                 // ignore
1514                 continue;
1515             }
1516 
1517             String categoryPrefix =
1518                     info.property.category().length() != 0 ? info.property.category() + ":" : "";
1519 
1520             if (info.returnType == int.class || info.returnType == byte.class) {
1521                 if (info.property.resolveId() && context != null) {
1522                     final int id = (Integer) value;
1523                     value = resolveId(context, id);
1524 
1525                 } else if (info.property.formatToHexString()) {
1526                     if (info.returnType == int.class) {
1527                         value = formatIntToHexString((Integer) value);
1528                     } else if (info.returnType == byte.class) {
1529                         value = "0x"
1530                                 + HexEncoding.encodeToString((Byte) value, true);
1531                     }
1532                 } else {
1533                     final ViewDebug.FlagToString[] flagsMapping = info.property.flagMapping();
1534                     if (flagsMapping.length > 0) {
1535                         final int intValue = (Integer) value;
1536                         final String valuePrefix =
1537                                 categoryPrefix + prefix + info.name + '_';
1538                         exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix);
1539                     }
1540 
1541                     final ViewDebug.IntToString[] mapping = info.property.mapping();
1542                     if (mapping.length > 0) {
1543                         final int intValue = (Integer) value;
1544                         boolean mapped = false;
1545                         int mappingCount = mapping.length;
1546                         for (int j = 0; j < mappingCount; j++) {
1547                             final ViewDebug.IntToString mapper = mapping[j];
1548                             if (mapper.from() == intValue) {
1549                                 value = mapper.to();
1550                                 mapped = true;
1551                                 break;
1552                             }
1553                         }
1554 
1555                         if (!mapped) {
1556                             value = intValue;
1557                         }
1558                     }
1559                 }
1560             } else if (info.returnType == int[].class) {
1561                 final int[] array = (int[]) value;
1562                 final String valuePrefix = categoryPrefix + prefix + info.name + '_';
1563                 exportUnrolledArray(context, out, info.property, array, valuePrefix,
1564                         info.entrySuffix);
1565 
1566                 continue;
1567             } else if (info.returnType == String[].class) {
1568                 final String[] array = (String[]) value;
1569                 if (info.property.hasAdjacentMapping() && array != null) {
1570                     for (int j = 0; j < array.length; j += 2) {
1571                         if (array[j] != null) {
1572                             writeEntry(out, categoryPrefix + prefix, array[j],
1573                                     info.entrySuffix, array[j + 1] == null ? "null" : array[j + 1]);
1574                         }
1575                     }
1576                 }
1577 
1578                 continue;
1579             } else if (!info.returnType.isPrimitive()) {
1580                 if (info.property.deepExport()) {
1581                     dumpViewProperties(context, value, out, prefix + info.property.prefix());
1582                     continue;
1583                 }
1584             }
1585 
1586             writeEntry(out, categoryPrefix + prefix, info.name, info.entrySuffix, value);
1587         }
1588     }
1589 
1590     private static void writeEntry(BufferedWriter out, String prefix, String name,
1591             String suffix, Object value) throws IOException {
1592 
1593         out.write(prefix);
1594         out.write(name);
1595         out.write(suffix);
1596         out.write("=");
1597         writeValue(out, value);
1598         out.write(' ');
1599     }
1600 
1601     private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping,
1602             int intValue, String prefix) throws IOException {
1603 
1604         final int count = mapping.length;
1605         for (int j = 0; j < count; j++) {
1606             final FlagToString flagMapping = mapping[j];
1607             final boolean ifTrue = flagMapping.outputIf();
1608             final int maskResult = intValue & flagMapping.mask();
1609             final boolean test = maskResult == flagMapping.equals();
1610             if ((test && ifTrue) || (!test && !ifTrue)) {
1611                 final String name = flagMapping.name();
1612                 final String value = formatIntToHexString(maskResult);
1613                 writeEntry(out, prefix, name, "", value);
1614             }
1615         }
1616     }
1617 
1618     /**
1619      * Converts an integer from a field that is mapped with {@link IntToString} to its string
1620      * representation.
1621      *
1622      * @param clazz The class the field is defined on.
1623      * @param field The field on which the {@link ExportedProperty} is defined on.
1624      * @param integer The value to convert.
1625      * @return The value converted into its string representation.
1626      * @hide
1627      */
1628     public static String intToString(Class<?> clazz, String field, int integer) {
1629         final IntToString[] mapping = getMapping(clazz, field);
1630         if (mapping == null) {
1631             return Integer.toString(integer);
1632         }
1633         final int count = mapping.length;
1634         for (int j = 0; j < count; j++) {
1635             final IntToString map = mapping[j];
1636             if (map.from() == integer) {
1637                 return map.to();
1638             }
1639         }
1640         return Integer.toString(integer);
1641     }
1642 
1643     /**
1644      * Converts a set of flags from a field that is mapped with {@link FlagToString} to its string
1645      * representation.
1646      *
1647      * @param clazz The class the field is defined on.
1648      * @param field The field on which the {@link ExportedProperty} is defined on.
1649      * @param flags The flags to convert.
1650      * @return The flags converted into their string representations.
1651      * @hide
1652      */
1653     public static String flagsToString(Class<?> clazz, String field, int flags) {
1654         final FlagToString[] mapping = getFlagMapping(clazz, field);
1655         if (mapping == null) {
1656             return Integer.toHexString(flags);
1657         }
1658         final StringBuilder result = new StringBuilder();
1659         final int count = mapping.length;
1660         for (int j = 0; j < count; j++) {
1661             final FlagToString flagMapping = mapping[j];
1662             final boolean ifTrue = flagMapping.outputIf();
1663             final int maskResult = flags & flagMapping.mask();
1664             final boolean test = maskResult == flagMapping.equals();
1665             if (test && ifTrue) {
1666                 final String name = flagMapping.name();
1667                 result.append(name).append(' ');
1668             }
1669         }
1670         if (result.length() > 0) {
1671             result.deleteCharAt(result.length() - 1);
1672         }
1673         return result.toString();
1674     }
1675 
1676     private static FlagToString[] getFlagMapping(Class<?> clazz, String field) {
1677         try {
1678             return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class)
1679                     .flagMapping();
1680         } catch (NoSuchFieldException e) {
1681             return null;
1682         }
1683     }
1684 
1685     private static IntToString[] getMapping(Class<?> clazz, String field) {
1686         try {
1687             return clazz.getDeclaredField(field).getAnnotation(ExportedProperty.class).mapping();
1688         } catch (NoSuchFieldException e) {
1689             return null;
1690         }
1691     }
1692 
1693     private static void exportUnrolledArray(Context context, BufferedWriter out,
1694             ExportedProperty property, int[] array, String prefix, String suffix)
1695             throws IOException {
1696 
1697         final IntToString[] indexMapping = property.indexMapping();
1698         final boolean hasIndexMapping = indexMapping.length > 0;
1699 
1700         final IntToString[] mapping = property.mapping();
1701         final boolean hasMapping = mapping.length > 0;
1702 
1703         final boolean resolveId = property.resolveId() && context != null;
1704         final int valuesCount = array.length;
1705 
1706         for (int j = 0; j < valuesCount; j++) {
1707             String name;
1708             String value = null;
1709 
1710             final int intValue = array[j];
1711 
1712             name = String.valueOf(j);
1713             if (hasIndexMapping) {
1714                 int mappingCount = indexMapping.length;
1715                 for (int k = 0; k < mappingCount; k++) {
1716                     final IntToString mapped = indexMapping[k];
1717                     if (mapped.from() == j) {
1718                         name = mapped.to();
1719                         break;
1720                     }
1721                 }
1722             }
1723 
1724             if (hasMapping) {
1725                 int mappingCount = mapping.length;
1726                 for (int k = 0; k < mappingCount; k++) {
1727                     final IntToString mapped = mapping[k];
1728                     if (mapped.from() == intValue) {
1729                         value = mapped.to();
1730                         break;
1731                     }
1732                 }
1733             }
1734 
1735             if (resolveId) {
1736                 if (value == null) value = (String) resolveId(context, intValue);
1737             } else {
1738                 value = String.valueOf(intValue);
1739             }
1740 
1741             writeEntry(out, prefix, name, suffix, value);
1742         }
1743     }
1744 
1745     static Object resolveId(Context context, int id) {
1746         Object fieldValue;
1747         final Resources resources = context.getResources();
1748         if (id >= 0) {
1749             try {
1750                 fieldValue = resources.getResourceTypeName(id) + '/' +
1751                         resources.getResourceEntryName(id);
1752             } catch (Resources.NotFoundException e) {
1753                 fieldValue = "id/" + formatIntToHexString(id);
1754             }
1755         } else {
1756             fieldValue = "NO_ID";
1757         }
1758         return fieldValue;
1759     }
1760 
1761     private static void writeValue(BufferedWriter out, Object value) throws IOException {
1762         if (value != null) {
1763             String output = "[EXCEPTION]";
1764             try {
1765                 output = value.toString().replace("\n", "\\n");
1766             } finally {
1767                 out.write(String.valueOf(output.length()));
1768                 out.write(",");
1769                 out.write(output);
1770             }
1771         } else {
1772             out.write("4,null");
1773         }
1774     }
1775 
1776     private static PropertyInfo<CapturedViewProperty, ?>[] getCapturedViewProperties(
1777             Class<?> klass) {
1778         if (sCapturedViewProperties == null) {
1779             sCapturedViewProperties = new HashMap<>();
1780         }
1781         final HashMap<Class<?>, PropertyInfo<CapturedViewProperty, ?>[]> map =
1782                 sCapturedViewProperties;
1783 
1784         PropertyInfo<CapturedViewProperty, ?>[] infos = map.get(klass);
1785         if (infos == null) {
1786             infos = convertToPropertyInfos(klass.getMethods(), klass.getFields(),
1787                     CapturedViewProperty.class);
1788             map.put(klass, infos);
1789         }
1790         return infos;
1791     }
1792 
1793     private static String exportCapturedViewProperties(Object obj, Class<?> klass, String prefix) {
1794         if (obj == null) {
1795             return "null";
1796         }
1797 
1798         StringBuilder sb = new StringBuilder();
1799 
1800         for (PropertyInfo<CapturedViewProperty, ?> pi : getCapturedViewProperties(klass)) {
1801             try {
1802                 Object methodValue = pi.invoke(obj);
1803 
1804                 if (pi.property.retrieveReturn()) {
1805                     //we are interested in the second level data only
1806                     sb.append(exportCapturedViewProperties(methodValue, pi.returnType,
1807                             pi.name + "#"));
1808                 } else {
1809                     sb.append(prefix).append(pi.name).append(pi.entrySuffix).append("=");
1810 
1811                     if (methodValue != null) {
1812                         final String value = methodValue.toString().replace("\n", "\\n");
1813                         sb.append(value);
1814                     } else {
1815                         sb.append("null");
1816                     }
1817                     sb.append(pi.valueSuffix).append(" ");
1818                 }
1819             } catch (Exception e) {
1820                 //It is OK here, we simply ignore this property
1821             }
1822         }
1823         return sb.toString();
1824     }
1825 
1826     /**
1827      * Dump view info for id based instrument test generation
1828      * (and possibly further data analysis). The results are dumped
1829      * to the log.
1830      * @param tag for log
1831      * @param view for dump
1832      */
1833     public static void dumpCapturedView(String tag, Object view) {
1834         Class<?> klass = view.getClass();
1835         StringBuilder sb = new StringBuilder(klass.getName() + ": ");
1836         sb.append(exportCapturedViewProperties(view, klass, ""));
1837         Log.d(tag, sb.toString());
1838     }
1839 
1840     private static void invokeViewMethod(View root, OutputStream clientStream, String[] params)
1841             throws IOException {
1842         BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024);
1843         try {
1844             if (params.length < 2) {
1845                 throw new IllegalArgumentException("Missing parameter");
1846             }
1847             View targetView = findView(root, params[0]);
1848             if (targetView == null) {
1849                 throw new IllegalArgumentException("View not found: " + params[0]);
1850             }
1851             String method = params[1];
1852             ByteBuffer args = ByteBuffer.wrap(params.length < 2
1853                     ? new byte[0]
1854                     : Base64.decode(params[2], Base64.NO_WRAP));
1855             byte[] result = invokeViewMethod(targetView, method, args);
1856             out.write("1");
1857             out.newLine();
1858             out.write(Base64.encodeToString(result, Base64.NO_WRAP));
1859             out.newLine();
1860         } catch (Exception e) {
1861             out.write("-1");
1862             out.newLine();
1863             out.write(e.getMessage());
1864             out.newLine();
1865         } finally {
1866             out.close();
1867         }
1868     }
1869 
1870     /**
1871      * Invoke a particular method on given view.
1872      * The given method is always invoked on the UI thread. The caller thread will stall until the
1873      * method invocation is complete. Returns an object equal to the result of the method
1874      * invocation, null if the method is declared to return void
1875      * @param params all the method parameters encoded in a byteArray
1876      * @throws Exception if the method invocation caused any exception
1877      * @hide
1878      */
1879     public static byte[] invokeViewMethod(View targetView, String methodName, ByteBuffer params)
1880             throws ViewMethodInvocationSerializationException {
1881         Class<?>[] argTypes;
1882         Object[] args;
1883         if (!params.hasRemaining()) {
1884             argTypes = new Class<?>[0];
1885             args = new Object[0];
1886         } else {
1887             int nArgs = params.getInt();
1888             argTypes = new Class<?>[nArgs];
1889             args = new Object[nArgs];
1890 
1891             deserializeMethodParameters(args, argTypes, params);
1892         }
1893 
1894         Method method;
1895         try {
1896             method = targetView.getClass().getMethod(methodName, argTypes);
1897         } catch (NoSuchMethodException e) {
1898             Log.e(TAG, "No such method: " + e.getMessage());
1899             throw new ViewMethodInvocationSerializationException(
1900                     "No such method: " + e.getMessage());
1901         }
1902 
1903         try {
1904             // Invoke the method on Views handler
1905             FutureTask<Object> task = new FutureTask<>(() -> method.invoke(targetView, args));
1906             targetView.post(task);
1907             Object result = task.get();
1908             Class<?> returnType = method.getReturnType();
1909             return serializeReturnValue(returnType, returnType.cast(result));
1910         } catch (Exception e) {
1911             Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage());
1912             String msg = e.getCause().getMessage();
1913             if (msg == null) {
1914                 msg = e.getCause().toString();
1915             }
1916             throw new RuntimeException(msg);
1917         }
1918     }
1919 
1920     /**
1921      * @hide
1922      */
1923     public static void setLayoutParameter(final View view, final String param, final int value)
1924             throws NoSuchFieldException, IllegalAccessException {
1925         final ViewGroup.LayoutParams p = view.getLayoutParams();
1926         final Field f = p.getClass().getField(param);
1927         if (f.getType() != int.class) {
1928             throw new RuntimeException("Only integer layout parameters can be set. Field "
1929                     + param + " is of type " + f.getType().getSimpleName());
1930         }
1931 
1932         f.set(p, Integer.valueOf(value));
1933 
1934         view.post(new Runnable() {
1935             @Override
1936             public void run() {
1937                 view.setLayoutParams(p);
1938             }
1939         });
1940     }
1941 
1942     /**
1943      * @hide
1944      */
1945     public static class SoftwareCanvasProvider implements CanvasProvider {
1946 
1947         private Canvas mCanvas;
1948         private Bitmap mBitmap;
1949         private boolean mEnabledHwFeaturesInSwMode;
1950 
1951         @Override
1952         public Canvas getCanvas(View view, int width, int height) {
1953             mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
1954                     width, height, Bitmap.Config.ARGB_8888);
1955             if (mBitmap == null) {
1956                 throw new OutOfMemoryError();
1957             }
1958             mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
1959 
1960             if (view.mAttachInfo != null) {
1961                 mCanvas = view.mAttachInfo.mCanvas;
1962             }
1963             if (mCanvas == null) {
1964                 mCanvas = new Canvas();
1965             }
1966             mEnabledHwFeaturesInSwMode = mCanvas.isHwFeaturesInSwModeEnabled();
1967             mCanvas.setBitmap(mBitmap);
1968             return mCanvas;
1969         }
1970 
1971         @Override
1972         public Bitmap createBitmap() {
1973             mCanvas.setBitmap(null);
1974             mCanvas.setHwFeaturesInSwModeEnabled(mEnabledHwFeaturesInSwMode);
1975             return mBitmap;
1976         }
1977     }
1978 
1979     /**
1980      * @hide
1981      */
1982     public static class HardwareCanvasProvider implements CanvasProvider {
1983         private Picture mPicture;
1984 
1985         @Override
1986         public Canvas getCanvas(View view, int width, int height) {
1987             mPicture = new Picture();
1988             return mPicture.beginRecording(width, height);
1989         }
1990 
1991         @Override
1992         public Bitmap createBitmap() {
1993             mPicture.endRecording();
1994             return Bitmap.createBitmap(mPicture);
1995         }
1996     }
1997 
1998     /**
1999      * @hide
2000      */
2001     public interface CanvasProvider {
2002 
2003         /**
2004          * Returns a canvas which can be used to draw {@param view}
2005          */
2006         Canvas getCanvas(View view, int width, int height);
2007 
2008         /**
2009          * Creates a bitmap from previously returned canvas
2010          * @return
2011          */
2012         Bitmap createBitmap();
2013     }
2014 
2015     /**
2016      * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in}
2017      * buffer.
2018      *
2019      * The length of {@code args} determines how many arguments are read. The {@code argTypes} must
2020      * be the same length, and will be set to the argument types of the data read.
2021      *
2022      * @hide
2023      */
2024     @VisibleForTesting
2025     public static void deserializeMethodParameters(
2026             Object[] args, Class<?>[] argTypes, ByteBuffer in) throws
2027             ViewMethodInvocationSerializationException {
2028         checkArgument(args.length == argTypes.length);
2029 
2030         for (int i = 0; i < args.length; i++) {
2031             char typeSignature = in.getChar();
2032             boolean isArray = typeSignature == SIG_ARRAY;
2033             if (isArray) {
2034                 char arrayType = in.getChar();
2035                 if (arrayType != SIG_BYTE) {
2036                     // This implementation only supports byte-arrays for now.
2037                     throw new ViewMethodInvocationSerializationException(
2038                             "Unsupported array parameter type (" + typeSignature
2039                                     + ") to invoke view method @argument " + i);
2040                 }
2041 
2042                 int arrayLength = in.getInt();
2043                 if (arrayLength > in.remaining()) {
2044                     // The sender did not actually sent the specified amount of bytes. This
2045                     // avoids a malformed packet to trigger an out-of-memory error.
2046                     throw new BufferUnderflowException();
2047                 }
2048 
2049                 byte[] byteArray = new byte[arrayLength];
2050                 in.get(byteArray);
2051 
2052                 argTypes[i] = byte[].class;
2053                 args[i] = byteArray;
2054             } else {
2055                 switch (typeSignature) {
2056                     case SIG_BOOLEAN:
2057                         argTypes[i] = boolean.class;
2058                         args[i] = in.get() != 0;
2059                         break;
2060                     case SIG_BYTE:
2061                         argTypes[i] = byte.class;
2062                         args[i] = in.get();
2063                         break;
2064                     case SIG_CHAR:
2065                         argTypes[i] = char.class;
2066                         args[i] = in.getChar();
2067                         break;
2068                     case SIG_SHORT:
2069                         argTypes[i] = short.class;
2070                         args[i] = in.getShort();
2071                         break;
2072                     case SIG_INT:
2073                         argTypes[i] = int.class;
2074                         args[i] = in.getInt();
2075                         break;
2076                     case SIG_LONG:
2077                         argTypes[i] = long.class;
2078                         args[i] = in.getLong();
2079                         break;
2080                     case SIG_FLOAT:
2081                         argTypes[i] = float.class;
2082                         args[i] = in.getFloat();
2083                         break;
2084                     case SIG_DOUBLE:
2085                         argTypes[i] = double.class;
2086                         args[i] = in.getDouble();
2087                         break;
2088                     case SIG_STRING: {
2089                         argTypes[i] = String.class;
2090                         int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort());
2091                         byte[] rawStringBuffer = new byte[stringUtf8ByteCount];
2092                         in.get(rawStringBuffer);
2093                         args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8);
2094                         break;
2095                     }
2096                     default:
2097                         Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature);
2098                         throw new ViewMethodInvocationSerializationException(
2099                                 "Unsupported parameter type (" + typeSignature
2100                                         + ") to invoke view method.");
2101                 }
2102             }
2103 
2104         }
2105     }
2106 
2107     /**
2108      * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD.
2109      * @hide
2110      */
2111     @VisibleForTesting
2112     public static byte[] serializeReturnValue(Class<?> type, Object value)
2113             throws ViewMethodInvocationSerializationException, IOException {
2114         ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024);
2115         DataOutputStream dos = new DataOutputStream(byteOutStream);
2116 
2117         if (type.isArray()) {
2118             if (!type.equals(byte[].class)) {
2119                 // Only byte arrays are supported currently.
2120                 throw new ViewMethodInvocationSerializationException(
2121                         "Unsupported array return type (" + type + ")");
2122             }
2123             byte[] byteArray = (byte[]) value;
2124             dos.writeChar(SIG_ARRAY);
2125             dos.writeChar(SIG_BYTE);
2126             dos.writeInt(byteArray.length);
2127             dos.write(byteArray);
2128         } else if (boolean.class.equals(type)) {
2129             dos.writeChar(SIG_BOOLEAN);
2130             dos.write((boolean) value ? 1 : 0);
2131         } else if (byte.class.equals(type)) {
2132             dos.writeChar(SIG_BYTE);
2133             dos.writeByte((byte) value);
2134         } else if (char.class.equals(type)) {
2135             dos.writeChar(SIG_CHAR);
2136             dos.writeChar((char) value);
2137         } else if (short.class.equals(type)) {
2138             dos.writeChar(SIG_SHORT);
2139             dos.writeShort((short) value);
2140         } else if (int.class.equals(type)) {
2141             dos.writeChar(SIG_INT);
2142             dos.writeInt((int) value);
2143         } else if (long.class.equals(type)) {
2144             dos.writeChar(SIG_LONG);
2145             dos.writeLong((long) value);
2146         } else if (double.class.equals(type)) {
2147             dos.writeChar(SIG_DOUBLE);
2148             dos.writeDouble((double) value);
2149         } else if (float.class.equals(type)) {
2150             dos.writeChar(SIG_FLOAT);
2151             dos.writeFloat((float) value);
2152         } else if (String.class.equals(type)) {
2153             dos.writeChar(SIG_STRING);
2154             dos.writeUTF(value != null ? (String) value : "");
2155         } else {
2156             dos.writeChar(SIG_VOID);
2157         }
2158 
2159         return byteOutStream.toByteArray();
2160     }
2161 
2162     // Prefixes for simple primitives. These match the JNI definitions.
2163     private static final char SIG_ARRAY = '[';
2164     private static final char SIG_BOOLEAN = 'Z';
2165     private static final char SIG_BYTE = 'B';
2166     private static final char SIG_SHORT = 'S';
2167     private static final char SIG_CHAR = 'C';
2168     private static final char SIG_INT = 'I';
2169     private static final char SIG_LONG = 'J';
2170     private static final char SIG_FLOAT = 'F';
2171     private static final char SIG_DOUBLE = 'D';
2172     private static final char SIG_VOID = 'V';
2173     // Prefixes for some commonly used objects
2174     private static final char SIG_STRING = 'R';
2175 
2176     /**
2177      * @hide
2178      */
2179     @VisibleForTesting
2180     public static class ViewMethodInvocationSerializationException extends Exception {
2181         ViewMethodInvocationSerializationException(String message) {
2182             super(message);
2183         }
2184     }
2185 }
2186