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