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