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 & {@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