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