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.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.Rect; 24 import android.os.Debug; 25 import android.os.Handler; 26 import android.os.RemoteException; 27 import android.util.DisplayMetrics; 28 import android.util.Log; 29 30 import java.io.BufferedOutputStream; 31 import java.io.BufferedWriter; 32 import java.io.ByteArrayOutputStream; 33 import java.io.DataOutputStream; 34 import java.io.IOException; 35 import java.io.OutputStream; 36 import java.io.OutputStreamWriter; 37 import java.lang.annotation.ElementType; 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.lang.annotation.Target; 41 import java.lang.reflect.AccessibleObject; 42 import java.lang.reflect.Field; 43 import java.lang.reflect.InvocationTargetException; 44 import java.lang.reflect.Method; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.concurrent.Callable; 48 import java.util.concurrent.CancellationException; 49 import java.util.concurrent.CountDownLatch; 50 import java.util.concurrent.ExecutionException; 51 import java.util.concurrent.FutureTask; 52 import java.util.concurrent.TimeoutException; 53 import java.util.concurrent.TimeUnit; 54 import java.util.concurrent.atomic.AtomicReference; 55 56 /** 57 * Various debugging/tracing tools related to {@link View} and the view hierarchy. 58 */ 59 public class ViewDebug { 60 /** 61 * @deprecated This flag is now unused 62 */ 63 @Deprecated 64 public static final boolean TRACE_HIERARCHY = false; 65 66 /** 67 * @deprecated This flag is now unused 68 */ 69 @Deprecated 70 public static final boolean TRACE_RECYCLER = false; 71 72 /** 73 * Enables detailed logging of drag/drop operations. 74 * @hide 75 */ 76 public static final boolean DEBUG_DRAG = false; 77 78 /** 79 * This annotation can be used to mark fields and methods to be dumped by 80 * the view server. Only non-void methods with no arguments can be annotated 81 * by this annotation. 82 */ 83 @Target({ ElementType.FIELD, ElementType.METHOD }) 84 @Retention(RetentionPolicy.RUNTIME) 85 public @interface ExportedProperty { 86 /** 87 * When resolveId is true, and if the annotated field/method return value 88 * is an int, the value is converted to an Android's resource name. 89 * 90 * @return true if the property's value must be transformed into an Android 91 * resource name, false otherwise 92 */ 93 boolean resolveId() default false; 94 95 /** 96 * A mapping can be defined to map int values to specific strings. For 97 * instance, View.getVisibility() returns 0, 4 or 8. However, these values 98 * actually mean VISIBLE, INVISIBLE and GONE. A mapping can be used to see 99 * these human readable values: 100 * 101 * <pre> 102 * @ViewDebug.ExportedProperty(mapping = { 103 * @ViewDebug.IntToString(from = 0, to = "VISIBLE"), 104 * @ViewDebug.IntToString(from = 4, to = "INVISIBLE"), 105 * @ViewDebug.IntToString(from = 8, to = "GONE") 106 * }) 107 * public int getVisibility() { ... 108 * <pre> 109 * 110 * @return An array of int to String mappings 111 * 112 * @see android.view.ViewDebug.IntToString 113 */ 114 IntToString[] mapping() default { }; 115 116 /** 117 * A mapping can be defined to map array indices to specific strings. 118 * A mapping can be used to see human readable values for the indices 119 * of an array: 120 * 121 * <pre> 122 * @ViewDebug.ExportedProperty(indexMapping = { 123 * @ViewDebug.IntToString(from = 0, to = "INVALID"), 124 * @ViewDebug.IntToString(from = 1, to = "FIRST"), 125 * @ViewDebug.IntToString(from = 2, to = "SECOND") 126 * }) 127 * private int[] mElements; 128 * <pre> 129 * 130 * @return An array of int to String mappings 131 * 132 * @see android.view.ViewDebug.IntToString 133 * @see #mapping() 134 */ 135 IntToString[] indexMapping() default { }; 136 137 /** 138 * A flags mapping can be defined to map flags encoded in an integer to 139 * specific strings. A mapping can be used to see human readable values 140 * for the flags of an integer: 141 * 142 * <pre> 143 * @ViewDebug.ExportedProperty(flagMapping = { 144 * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = ENABLED, name = "ENABLED"), 145 * @ViewDebug.FlagToString(mask = ENABLED_MASK, equals = DISABLED, name = "DISABLED"), 146 * }) 147 * private int mFlags; 148 * <pre> 149 * 150 * A specified String is output when the following is true: 151 * 152 * @return An array of int to String mappings 153 */ 154 FlagToString[] flagMapping() default { }; 155 156 /** 157 * When deep export is turned on, this property is not dumped. Instead, the 158 * properties contained in this property are dumped. Each child property 159 * is prefixed with the name of this property. 160 * 161 * @return true if the properties of this property should be dumped 162 * 163 * @see #prefix() 164 */ 165 boolean deepExport() default false; 166 167 /** 168 * The prefix to use on child properties when deep export is enabled 169 * 170 * @return a prefix as a String 171 * 172 * @see #deepExport() 173 */ 174 String prefix() default ""; 175 176 /** 177 * Specifies the category the property falls into, such as measurement, 178 * layout, drawing, etc. 179 * 180 * @return the category as String 181 */ 182 String category() default ""; 183 } 184 185 /** 186 * Defines a mapping from an int value to a String. Such a mapping can be used 187 * in an @ExportedProperty to provide more meaningful values to the end user. 188 * 189 * @see android.view.ViewDebug.ExportedProperty 190 */ 191 @Target({ ElementType.TYPE }) 192 @Retention(RetentionPolicy.RUNTIME) 193 public @interface IntToString { 194 /** 195 * The original int value to map to a String. 196 * 197 * @return An arbitrary int value. 198 */ 199 int from(); 200 201 /** 202 * The String to use in place of the original int value. 203 * 204 * @return An arbitrary non-null String. 205 */ 206 String to(); 207 } 208 209 /** 210 * Defines a mapping from a flag to a String. Such a mapping can be used 211 * in an @ExportedProperty to provide more meaningful values to the end user. 212 * 213 * @see android.view.ViewDebug.ExportedProperty 214 */ 215 @Target({ ElementType.TYPE }) 216 @Retention(RetentionPolicy.RUNTIME) 217 public @interface FlagToString { 218 /** 219 * The mask to apply to the original value. 220 * 221 * @return An arbitrary int value. 222 */ 223 int mask(); 224 225 /** 226 * The value to compare to the result of: 227 * <code>original value & {@link #mask()}</code>. 228 * 229 * @return An arbitrary value. 230 */ 231 int equals(); 232 233 /** 234 * The String to use in place of the original int value. 235 * 236 * @return An arbitrary non-null String. 237 */ 238 String name(); 239 240 /** 241 * Indicates whether to output the flag when the test is true, 242 * or false. Defaults to true. 243 */ 244 boolean outputIf() default true; 245 } 246 247 /** 248 * This annotation can be used to mark fields and methods to be dumped when 249 * the view is captured. Methods with this annotation must have no arguments 250 * and must return a valid type of data. 251 */ 252 @Target({ ElementType.FIELD, ElementType.METHOD }) 253 @Retention(RetentionPolicy.RUNTIME) 254 public @interface CapturedViewProperty { 255 /** 256 * When retrieveReturn is true, we need to retrieve second level methods 257 * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() 258 * we will set retrieveReturn = true on the annotation of 259 * myView.getFirstLevelMethod() 260 * @return true if we need the second level methods 261 */ 262 boolean retrieveReturn() default false; 263 } 264 265 /** 266 * Allows a View to inject custom children into HierarchyViewer. For example, 267 * WebView uses this to add its internal layer tree as a child to itself 268 * @hide 269 */ 270 public interface HierarchyHandler { 271 /** 272 * Dumps custom children to hierarchy viewer. 273 * See ViewDebug.dumpViewWithProperties(Context, View, BufferedWriter, int) 274 * for the format 275 * 276 * An empty implementation should simply do nothing 277 * 278 * @param out The output writer 279 * @param level The indentation level 280 */ 281 public void dumpViewHierarchyWithProperties(BufferedWriter out, int level); 282 283 /** 284 * Returns a View to enable grabbing screenshots from custom children 285 * returned in dumpViewHierarchyWithProperties. 286 * 287 * @param className The className of the view to find 288 * @param hashCode The hashCode of the view to find 289 * @return the View to capture from, or null if not found 290 */ 291 public View findHierarchyView(String className, int hashCode); 292 } 293 294 private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; 295 private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; 296 297 // Maximum delay in ms after which we stop trying to capture a View's drawing 298 private static final int CAPTURE_TIMEOUT = 4000; 299 300 private static final String REMOTE_COMMAND_CAPTURE = "CAPTURE"; 301 private static final String REMOTE_COMMAND_DUMP = "DUMP"; 302 private static final String REMOTE_COMMAND_INVALIDATE = "INVALIDATE"; 303 private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; 304 private static final String REMOTE_PROFILE = "PROFILE"; 305 private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; 306 private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; 307 308 private static HashMap<Class<?>, Field[]> sFieldsForClasses; 309 private static HashMap<Class<?>, Method[]> sMethodsForClasses; 310 private static HashMap<AccessibleObject, ExportedProperty> sAnnotations; 311 312 /** 313 * @deprecated This enum is now unused 314 */ 315 @Deprecated 316 public enum HierarchyTraceType { 317 INVALIDATE, 318 INVALIDATE_CHILD, 319 INVALIDATE_CHILD_IN_PARENT, 320 REQUEST_LAYOUT, 321 ON_LAYOUT, 322 ON_MEASURE, 323 DRAW, 324 BUILD_CACHE 325 } 326 327 /** 328 * @deprecated This enum is now unused 329 */ 330 @Deprecated 331 public enum RecyclerTraceType { 332 NEW_VIEW, 333 BIND_VIEW, 334 RECYCLE_FROM_ACTIVE_HEAP, 335 RECYCLE_FROM_SCRAP_HEAP, 336 MOVE_TO_SCRAP_HEAP, 337 MOVE_FROM_ACTIVE_TO_SCRAP_HEAP 338 } 339 340 /** 341 * Returns the number of instanciated Views. 342 * 343 * @return The number of Views instanciated in the current process. 344 * 345 * @hide 346 */ 347 public static long getViewInstanceCount() { 348 return Debug.countInstancesOfClass(View.class); 349 } 350 351 /** 352 * Returns the number of instanciated ViewAncestors. 353 * 354 * @return The number of ViewAncestors instanciated in the current process. 355 * 356 * @hide 357 */ 358 public static long getViewRootImplCount() { 359 return Debug.countInstancesOfClass(ViewRootImpl.class); 360 } 361 362 /** 363 * @deprecated This method is now unused and invoking it is a no-op 364 */ 365 @Deprecated 366 @SuppressWarnings({ "UnusedParameters", "deprecation" }) 367 public static void trace(View view, RecyclerTraceType type, int... parameters) { 368 } 369 370 /** 371 * @deprecated This method is now unused and invoking it is a no-op 372 */ 373 @Deprecated 374 @SuppressWarnings("UnusedParameters") 375 public static void startRecyclerTracing(String prefix, View view) { 376 } 377 378 /** 379 * @deprecated This method is now unused and invoking it is a no-op 380 */ 381 @Deprecated 382 @SuppressWarnings("UnusedParameters") 383 public static void stopRecyclerTracing() { 384 } 385 386 /** 387 * @deprecated This method is now unused and invoking it is a no-op 388 */ 389 @Deprecated 390 @SuppressWarnings({ "UnusedParameters", "deprecation" }) 391 public static void trace(View view, HierarchyTraceType type) { 392 } 393 394 /** 395 * @deprecated This method is now unused and invoking it is a no-op 396 */ 397 @Deprecated 398 @SuppressWarnings("UnusedParameters") 399 public static void startHierarchyTracing(String prefix, View view) { 400 } 401 402 /** 403 * @deprecated This method is now unused and invoking it is a no-op 404 */ 405 @Deprecated 406 public static void stopHierarchyTracing() { 407 } 408 409 static void dispatchCommand(View view, String command, String parameters, 410 OutputStream clientStream) throws IOException { 411 412 // Paranoid but safe... 413 view = view.getRootView(); 414 415 if (REMOTE_COMMAND_DUMP.equalsIgnoreCase(command)) { 416 dump(view, false, true, clientStream); 417 } else if (REMOTE_COMMAND_CAPTURE_LAYERS.equalsIgnoreCase(command)) { 418 captureLayers(view, new DataOutputStream(clientStream)); 419 } else { 420 final String[] params = parameters.split(" "); 421 if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { 422 capture(view, clientStream, params[0]); 423 } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { 424 outputDisplayList(view, params[0]); 425 } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { 426 invalidate(view, params[0]); 427 } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { 428 requestLayout(view, params[0]); 429 } else if (REMOTE_PROFILE.equalsIgnoreCase(command)) { 430 profile(view, clientStream, params[0]); 431 } 432 } 433 } 434 435 /** @hide */ 436 public static View findView(View root, String parameter) { 437 // Look by type/hashcode 438 if (parameter.indexOf('@') != -1) { 439 final String[] ids = parameter.split("@"); 440 final String className = ids[0]; 441 final int hashCode = (int) Long.parseLong(ids[1], 16); 442 443 View view = root.getRootView(); 444 if (view instanceof ViewGroup) { 445 return findView((ViewGroup) view, className, hashCode); 446 } 447 } else { 448 // Look by id 449 final int id = root.getResources().getIdentifier(parameter, null, null); 450 return root.getRootView().findViewById(id); 451 } 452 453 return null; 454 } 455 456 private static void invalidate(View root, String parameter) { 457 final View view = findView(root, parameter); 458 if (view != null) { 459 view.postInvalidate(); 460 } 461 } 462 463 private static void requestLayout(View root, String parameter) { 464 final View view = findView(root, parameter); 465 if (view != null) { 466 root.post(new Runnable() { 467 public void run() { 468 view.requestLayout(); 469 } 470 }); 471 } 472 } 473 474 private static void profile(View root, OutputStream clientStream, String parameter) 475 throws IOException { 476 477 final View view = findView(root, parameter); 478 BufferedWriter out = null; 479 try { 480 out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); 481 482 if (view != null) { 483 profileViewAndChildren(view, out); 484 } else { 485 out.write("-1 -1 -1"); 486 out.newLine(); 487 } 488 out.write("DONE."); 489 out.newLine(); 490 } catch (Exception e) { 491 android.util.Log.w("View", "Problem profiling the view:", e); 492 } finally { 493 if (out != null) { 494 out.close(); 495 } 496 } 497 } 498 499 /** @hide */ 500 public static void profileViewAndChildren(final View view, BufferedWriter out) 501 throws IOException { 502 profileViewAndChildren(view, out, true); 503 } 504 505 private static void profileViewAndChildren(final View view, BufferedWriter out, boolean root) 506 throws IOException { 507 508 long durationMeasure = 509 (root || (view.mPrivateFlags & View.PFLAG_MEASURED_DIMENSION_SET) != 0) 510 ? profileViewOperation(view, new ViewOperation<Void>() { 511 public Void[] pre() { 512 forceLayout(view); 513 return null; 514 } 515 516 private void forceLayout(View view) { 517 view.forceLayout(); 518 if (view instanceof ViewGroup) { 519 ViewGroup group = (ViewGroup) view; 520 final int count = group.getChildCount(); 521 for (int i = 0; i < count; i++) { 522 forceLayout(group.getChildAt(i)); 523 } 524 } 525 } 526 527 public void run(Void... data) { 528 view.measure(view.mOldWidthMeasureSpec, view.mOldHeightMeasureSpec); 529 } 530 531 public void post(Void... data) { 532 } 533 }) 534 : 0; 535 long durationLayout = 536 (root || (view.mPrivateFlags & View.PFLAG_LAYOUT_REQUIRED) != 0) 537 ? profileViewOperation(view, new ViewOperation<Void>() { 538 public Void[] pre() { 539 return null; 540 } 541 542 public void run(Void... data) { 543 view.layout(view.mLeft, view.mTop, view.mRight, view.mBottom); 544 } 545 546 public void post(Void... data) { 547 } 548 }) : 0; 549 long durationDraw = 550 (root || !view.willNotDraw() || (view.mPrivateFlags & View.PFLAG_DRAWN) != 0) 551 ? profileViewOperation(view, new ViewOperation<Object>() { 552 public Object[] pre() { 553 final DisplayMetrics metrics = 554 (view != null && view.getResources() != null) ? 555 view.getResources().getDisplayMetrics() : null; 556 final Bitmap bitmap = metrics != null ? 557 Bitmap.createBitmap(metrics, metrics.widthPixels, 558 metrics.heightPixels, Bitmap.Config.RGB_565) : null; 559 final Canvas canvas = bitmap != null ? new Canvas(bitmap) : null; 560 return new Object[] { 561 bitmap, canvas 562 }; 563 } 564 565 public void run(Object... data) { 566 if (data[1] != null) { 567 view.draw((Canvas) data[1]); 568 } 569 } 570 571 public void post(Object... data) { 572 if (data[1] != null) { 573 ((Canvas) data[1]).setBitmap(null); 574 } 575 if (data[0] != null) { 576 ((Bitmap) data[0]).recycle(); 577 } 578 } 579 }) : 0; 580 out.write(String.valueOf(durationMeasure)); 581 out.write(' '); 582 out.write(String.valueOf(durationLayout)); 583 out.write(' '); 584 out.write(String.valueOf(durationDraw)); 585 out.newLine(); 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 profileViewAndChildren(group.getChildAt(i), out, false); 591 } 592 } 593 } 594 595 interface ViewOperation<T> { 596 T[] pre(); 597 void run(T... data); 598 void post(T... data); 599 } 600 601 private static <T> long profileViewOperation(View view, final ViewOperation<T> operation) { 602 final CountDownLatch latch = new CountDownLatch(1); 603 final long[] duration = new long[1]; 604 605 view.post(new Runnable() { 606 public void run() { 607 try { 608 T[] data = operation.pre(); 609 long start = Debug.threadCpuTimeNanos(); 610 //noinspection unchecked 611 operation.run(data); 612 duration[0] = Debug.threadCpuTimeNanos() - start; 613 //noinspection unchecked 614 operation.post(data); 615 } finally { 616 latch.countDown(); 617 } 618 } 619 }); 620 621 try { 622 if (!latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS)) { 623 Log.w("View", "Could not complete the profiling of the view " + view); 624 return -1; 625 } 626 } catch (InterruptedException e) { 627 Log.w("View", "Could not complete the profiling of the view " + view); 628 Thread.currentThread().interrupt(); 629 return -1; 630 } 631 632 return duration[0]; 633 } 634 635 /** @hide */ 636 public static void captureLayers(View root, final DataOutputStream clientStream) 637 throws IOException { 638 639 try { 640 Rect outRect = new Rect(); 641 try { 642 root.mAttachInfo.mSession.getDisplayFrame(root.mAttachInfo.mWindow, outRect); 643 } catch (RemoteException e) { 644 // Ignore 645 } 646 647 clientStream.writeInt(outRect.width()); 648 clientStream.writeInt(outRect.height()); 649 650 captureViewLayer(root, clientStream, true); 651 652 clientStream.write(2); 653 } finally { 654 clientStream.close(); 655 } 656 } 657 658 private static void captureViewLayer(View view, DataOutputStream clientStream, boolean visible) 659 throws IOException { 660 661 final boolean localVisible = view.getVisibility() == View.VISIBLE && visible; 662 663 if ((view.mPrivateFlags & View.PFLAG_SKIP_DRAW) != View.PFLAG_SKIP_DRAW) { 664 final int id = view.getId(); 665 String name = view.getClass().getSimpleName(); 666 if (id != View.NO_ID) { 667 name = resolveId(view.getContext(), id).toString(); 668 } 669 670 clientStream.write(1); 671 clientStream.writeUTF(name); 672 clientStream.writeByte(localVisible ? 1 : 0); 673 674 int[] position = new int[2]; 675 // XXX: Should happen on the UI thread 676 view.getLocationInWindow(position); 677 678 clientStream.writeInt(position[0]); 679 clientStream.writeInt(position[1]); 680 clientStream.flush(); 681 682 Bitmap b = performViewCapture(view, true); 683 if (b != null) { 684 ByteArrayOutputStream arrayOut = new ByteArrayOutputStream(b.getWidth() * 685 b.getHeight() * 2); 686 b.compress(Bitmap.CompressFormat.PNG, 100, arrayOut); 687 clientStream.writeInt(arrayOut.size()); 688 arrayOut.writeTo(clientStream); 689 } 690 clientStream.flush(); 691 } 692 693 if (view instanceof ViewGroup) { 694 ViewGroup group = (ViewGroup) view; 695 int count = group.getChildCount(); 696 697 for (int i = 0; i < count; i++) { 698 captureViewLayer(group.getChildAt(i), clientStream, localVisible); 699 } 700 } 701 } 702 703 private static void outputDisplayList(View root, String parameter) throws IOException { 704 final View view = findView(root, parameter); 705 view.getViewRootImpl().outputDisplayList(view); 706 } 707 708 /** @hide */ 709 public static void outputDisplayList(View root, View target) { 710 root.getViewRootImpl().outputDisplayList(target); 711 } 712 713 private static void capture(View root, final OutputStream clientStream, String parameter) 714 throws IOException { 715 716 final View captureView = findView(root, parameter); 717 capture(root, clientStream, captureView); 718 } 719 720 /** @hide */ 721 public static void capture(View root, final OutputStream clientStream, View captureView) 722 throws IOException { 723 Bitmap b = performViewCapture(captureView, false); 724 725 if (b == null) { 726 Log.w("View", "Failed to create capture bitmap!"); 727 // Send an empty one so that it doesn't get stuck waiting for 728 // something. 729 b = Bitmap.createBitmap(root.getResources().getDisplayMetrics(), 730 1, 1, Bitmap.Config.ARGB_8888); 731 } 732 733 BufferedOutputStream out = null; 734 try { 735 out = new BufferedOutputStream(clientStream, 32 * 1024); 736 b.compress(Bitmap.CompressFormat.PNG, 100, out); 737 out.flush(); 738 } finally { 739 if (out != null) { 740 out.close(); 741 } 742 b.recycle(); 743 } 744 } 745 746 private static Bitmap performViewCapture(final View captureView, final boolean skpiChildren) { 747 if (captureView != null) { 748 final CountDownLatch latch = new CountDownLatch(1); 749 final Bitmap[] cache = new Bitmap[1]; 750 751 captureView.post(new Runnable() { 752 public void run() { 753 try { 754 cache[0] = captureView.createSnapshot( 755 Bitmap.Config.ARGB_8888, 0, skpiChildren); 756 } catch (OutOfMemoryError e) { 757 Log.w("View", "Out of memory for bitmap"); 758 } finally { 759 latch.countDown(); 760 } 761 } 762 }); 763 764 try { 765 latch.await(CAPTURE_TIMEOUT, TimeUnit.MILLISECONDS); 766 return cache[0]; 767 } catch (InterruptedException e) { 768 Log.w("View", "Could not complete the capture of the view " + captureView); 769 Thread.currentThread().interrupt(); 770 } 771 } 772 773 return null; 774 } 775 776 /** 777 * Dumps the view hierarchy starting from the given view. 778 * @hide 779 */ 780 public static void dump(View root, boolean skipChildren, boolean includeProperties, 781 OutputStream clientStream) throws IOException { 782 BufferedWriter out = null; 783 try { 784 out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); 785 View view = root.getRootView(); 786 if (view instanceof ViewGroup) { 787 ViewGroup group = (ViewGroup) view; 788 dumpViewHierarchy(group.getContext(), group, out, 0, 789 skipChildren, includeProperties); 790 } 791 out.write("DONE."); 792 out.newLine(); 793 } catch (Exception e) { 794 android.util.Log.w("View", "Problem dumping the view:", e); 795 } finally { 796 if (out != null) { 797 out.close(); 798 } 799 } 800 } 801 802 private static View findView(ViewGroup group, String className, int hashCode) { 803 if (isRequestedView(group, className, hashCode)) { 804 return group; 805 } 806 807 final int count = group.getChildCount(); 808 for (int i = 0; i < count; i++) { 809 final View view = group.getChildAt(i); 810 if (view instanceof ViewGroup) { 811 final View found = findView((ViewGroup) view, className, hashCode); 812 if (found != null) { 813 return found; 814 } 815 } else if (isRequestedView(view, className, hashCode)) { 816 return view; 817 } 818 if (view instanceof HierarchyHandler) { 819 final View found = ((HierarchyHandler)view) 820 .findHierarchyView(className, hashCode); 821 if (found != null) { 822 return found; 823 } 824 } 825 } 826 827 return null; 828 } 829 830 private static boolean isRequestedView(View view, String className, int hashCode) { 831 return view.getClass().getName().equals(className) && view.hashCode() == hashCode; 832 } 833 834 private static void dumpViewHierarchy(Context context, ViewGroup group, 835 BufferedWriter out, int level, boolean skipChildren, boolean includeProperties) { 836 if (!dumpView(context, group, out, level, includeProperties)) { 837 return; 838 } 839 840 if (skipChildren) { 841 return; 842 } 843 844 final int count = group.getChildCount(); 845 for (int i = 0; i < count; i++) { 846 final View view = group.getChildAt(i); 847 if (view instanceof ViewGroup) { 848 dumpViewHierarchy(context, (ViewGroup) view, out, level + 1, skipChildren, 849 includeProperties); 850 } else { 851 dumpView(context, view, out, level + 1, includeProperties); 852 } 853 } 854 if (group instanceof HierarchyHandler) { 855 ((HierarchyHandler)group).dumpViewHierarchyWithProperties(out, level + 1); 856 } 857 } 858 859 private static boolean dumpView(Context context, View view, 860 BufferedWriter out, int level, boolean includeProperties) { 861 862 try { 863 for (int i = 0; i < level; i++) { 864 out.write(' '); 865 } 866 out.write(view.getClass().getName()); 867 out.write('@'); 868 out.write(Integer.toHexString(view.hashCode())); 869 out.write(' '); 870 if (includeProperties) { 871 dumpViewProperties(context, view, out); 872 } 873 out.newLine(); 874 } catch (IOException e) { 875 Log.w("View", "Error while dumping hierarchy tree"); 876 return false; 877 } 878 return true; 879 } 880 881 private static Field[] getExportedPropertyFields(Class<?> klass) { 882 if (sFieldsForClasses == null) { 883 sFieldsForClasses = new HashMap<Class<?>, Field[]>(); 884 } 885 if (sAnnotations == null) { 886 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 887 } 888 889 final HashMap<Class<?>, Field[]> map = sFieldsForClasses; 890 891 Field[] fields = map.get(klass); 892 if (fields != null) { 893 return fields; 894 } 895 896 final ArrayList<Field> foundFields = new ArrayList<Field>(); 897 fields = klass.getDeclaredFields(); 898 899 int count = fields.length; 900 for (int i = 0; i < count; i++) { 901 final Field field = fields[i]; 902 if (field.isAnnotationPresent(ExportedProperty.class)) { 903 field.setAccessible(true); 904 foundFields.add(field); 905 sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); 906 } 907 } 908 909 fields = foundFields.toArray(new Field[foundFields.size()]); 910 map.put(klass, fields); 911 912 return fields; 913 } 914 915 private static Method[] getExportedPropertyMethods(Class<?> klass) { 916 if (sMethodsForClasses == null) { 917 sMethodsForClasses = new HashMap<Class<?>, Method[]>(100); 918 } 919 if (sAnnotations == null) { 920 sAnnotations = new HashMap<AccessibleObject, ExportedProperty>(512); 921 } 922 923 final HashMap<Class<?>, Method[]> map = sMethodsForClasses; 924 925 Method[] methods = map.get(klass); 926 if (methods != null) { 927 return methods; 928 } 929 930 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 931 methods = klass.getDeclaredMethods(); 932 933 int count = methods.length; 934 for (int i = 0; i < count; i++) { 935 final Method method = methods[i]; 936 if (method.getParameterTypes().length == 0 && 937 method.isAnnotationPresent(ExportedProperty.class) && 938 method.getReturnType() != Void.class) { 939 method.setAccessible(true); 940 foundMethods.add(method); 941 sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); 942 } 943 } 944 945 methods = foundMethods.toArray(new Method[foundMethods.size()]); 946 map.put(klass, methods); 947 948 return methods; 949 } 950 951 private static void dumpViewProperties(Context context, Object view, 952 BufferedWriter out) throws IOException { 953 954 dumpViewProperties(context, view, out, ""); 955 } 956 957 private static void dumpViewProperties(Context context, Object view, 958 BufferedWriter out, String prefix) throws IOException { 959 960 if (view == null) { 961 out.write(prefix + "=4,null "); 962 return; 963 } 964 965 Class<?> klass = view.getClass(); 966 do { 967 exportFields(context, view, out, klass, prefix); 968 exportMethods(context, view, out, klass, prefix); 969 klass = klass.getSuperclass(); 970 } while (klass != Object.class); 971 } 972 973 private static Object callMethodOnAppropriateTheadBlocking(final Method method, 974 final Object object) throws IllegalAccessException, InvocationTargetException, 975 TimeoutException { 976 if (!(object instanceof View)) { 977 return method.invoke(object, (Object[]) null); 978 } 979 980 final View view = (View) object; 981 Callable<Object> callable = new Callable<Object>() { 982 @Override 983 public Object call() throws IllegalAccessException, InvocationTargetException { 984 return method.invoke(view, (Object[]) null); 985 } 986 }; 987 FutureTask<Object> future = new FutureTask<Object>(callable); 988 // Try to use the handler provided by the view 989 Handler handler = view.getHandler(); 990 // Fall back on using the main thread 991 if (handler == null) { 992 handler = new Handler(android.os.Looper.getMainLooper()); 993 } 994 handler.post(future); 995 while (true) { 996 try { 997 return future.get(CAPTURE_TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS); 998 } catch (ExecutionException e) { 999 Throwable t = e.getCause(); 1000 if (t instanceof IllegalAccessException) { 1001 throw (IllegalAccessException)t; 1002 } 1003 if (t instanceof InvocationTargetException) { 1004 throw (InvocationTargetException)t; 1005 } 1006 throw new RuntimeException("Unexpected exception", t); 1007 } catch (InterruptedException e) { 1008 // Call get again 1009 } catch (CancellationException e) { 1010 throw new RuntimeException("Unexpected cancellation exception", e); 1011 } 1012 } 1013 } 1014 1015 private static void exportMethods(Context context, Object view, BufferedWriter out, 1016 Class<?> klass, String prefix) throws IOException { 1017 1018 final Method[] methods = getExportedPropertyMethods(klass); 1019 1020 int count = methods.length; 1021 for (int i = 0; i < count; i++) { 1022 final Method method = methods[i]; 1023 //noinspection EmptyCatchBlock 1024 try { 1025 Object methodValue = callMethodOnAppropriateTheadBlocking(method, view); 1026 final Class<?> returnType = method.getReturnType(); 1027 final ExportedProperty property = sAnnotations.get(method); 1028 String categoryPrefix = 1029 property.category().length() != 0 ? property.category() + ":" : ""; 1030 1031 if (returnType == int.class) { 1032 1033 if (property.resolveId() && context != null) { 1034 final int id = (Integer) methodValue; 1035 methodValue = resolveId(context, id); 1036 } else { 1037 final FlagToString[] flagsMapping = property.flagMapping(); 1038 if (flagsMapping.length > 0) { 1039 final int intValue = (Integer) methodValue; 1040 final String valuePrefix = 1041 categoryPrefix + prefix + method.getName() + '_'; 1042 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 1043 } 1044 1045 final IntToString[] mapping = property.mapping(); 1046 if (mapping.length > 0) { 1047 final int intValue = (Integer) methodValue; 1048 boolean mapped = false; 1049 int mappingCount = mapping.length; 1050 for (int j = 0; j < mappingCount; j++) { 1051 final IntToString mapper = mapping[j]; 1052 if (mapper.from() == intValue) { 1053 methodValue = mapper.to(); 1054 mapped = true; 1055 break; 1056 } 1057 } 1058 1059 if (!mapped) { 1060 methodValue = intValue; 1061 } 1062 } 1063 } 1064 } else if (returnType == int[].class) { 1065 final int[] array = (int[]) methodValue; 1066 final String valuePrefix = categoryPrefix + prefix + method.getName() + '_'; 1067 final String suffix = "()"; 1068 1069 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1070 1071 // Probably want to return here, same as for fields. 1072 return; 1073 } else if (!returnType.isPrimitive()) { 1074 if (property.deepExport()) { 1075 dumpViewProperties(context, methodValue, out, prefix + property.prefix()); 1076 continue; 1077 } 1078 } 1079 1080 writeEntry(out, categoryPrefix + prefix, method.getName(), "()", methodValue); 1081 } catch (IllegalAccessException e) { 1082 } catch (InvocationTargetException e) { 1083 } catch (TimeoutException e) { 1084 } 1085 } 1086 } 1087 1088 private static void exportFields(Context context, Object view, BufferedWriter out, 1089 Class<?> klass, String prefix) throws IOException { 1090 1091 final Field[] fields = getExportedPropertyFields(klass); 1092 1093 int count = fields.length; 1094 for (int i = 0; i < count; i++) { 1095 final Field field = fields[i]; 1096 1097 //noinspection EmptyCatchBlock 1098 try { 1099 Object fieldValue = null; 1100 final Class<?> type = field.getType(); 1101 final ExportedProperty property = sAnnotations.get(field); 1102 String categoryPrefix = 1103 property.category().length() != 0 ? property.category() + ":" : ""; 1104 1105 if (type == int.class || type == byte.class) { 1106 if (property.resolveId() && context != null) { 1107 final int id = field.getInt(view); 1108 fieldValue = resolveId(context, id); 1109 } else { 1110 final FlagToString[] flagsMapping = property.flagMapping(); 1111 if (flagsMapping.length > 0) { 1112 final int intValue = field.getInt(view); 1113 final String valuePrefix = 1114 categoryPrefix + prefix + field.getName() + '_'; 1115 exportUnrolledFlags(out, flagsMapping, intValue, valuePrefix); 1116 } 1117 1118 final IntToString[] mapping = property.mapping(); 1119 if (mapping.length > 0) { 1120 final int intValue = field.getInt(view); 1121 int mappingCount = mapping.length; 1122 for (int j = 0; j < mappingCount; j++) { 1123 final IntToString mapped = mapping[j]; 1124 if (mapped.from() == intValue) { 1125 fieldValue = mapped.to(); 1126 break; 1127 } 1128 } 1129 1130 if (fieldValue == null) { 1131 fieldValue = intValue; 1132 } 1133 } 1134 } 1135 } else if (type == int[].class) { 1136 final int[] array = (int[]) field.get(view); 1137 final String valuePrefix = categoryPrefix + prefix + field.getName() + '_'; 1138 final String suffix = ""; 1139 1140 exportUnrolledArray(context, out, property, array, valuePrefix, suffix); 1141 1142 // We exit here! 1143 return; 1144 } else if (!type.isPrimitive()) { 1145 if (property.deepExport()) { 1146 dumpViewProperties(context, field.get(view), out, prefix + 1147 property.prefix()); 1148 continue; 1149 } 1150 } 1151 1152 if (fieldValue == null) { 1153 fieldValue = field.get(view); 1154 } 1155 1156 writeEntry(out, categoryPrefix + prefix, field.getName(), "", fieldValue); 1157 } catch (IllegalAccessException e) { 1158 } 1159 } 1160 } 1161 1162 private static void writeEntry(BufferedWriter out, String prefix, String name, 1163 String suffix, Object value) throws IOException { 1164 1165 out.write(prefix); 1166 out.write(name); 1167 out.write(suffix); 1168 out.write("="); 1169 writeValue(out, value); 1170 out.write(' '); 1171 } 1172 1173 private static void exportUnrolledFlags(BufferedWriter out, FlagToString[] mapping, 1174 int intValue, String prefix) throws IOException { 1175 1176 final int count = mapping.length; 1177 for (int j = 0; j < count; j++) { 1178 final FlagToString flagMapping = mapping[j]; 1179 final boolean ifTrue = flagMapping.outputIf(); 1180 final int maskResult = intValue & flagMapping.mask(); 1181 final boolean test = maskResult == flagMapping.equals(); 1182 if ((test && ifTrue) || (!test && !ifTrue)) { 1183 final String name = flagMapping.name(); 1184 final String value = "0x" + Integer.toHexString(maskResult); 1185 writeEntry(out, prefix, name, "", value); 1186 } 1187 } 1188 } 1189 1190 private static void exportUnrolledArray(Context context, BufferedWriter out, 1191 ExportedProperty property, int[] array, String prefix, String suffix) 1192 throws IOException { 1193 1194 final IntToString[] indexMapping = property.indexMapping(); 1195 final boolean hasIndexMapping = indexMapping.length > 0; 1196 1197 final IntToString[] mapping = property.mapping(); 1198 final boolean hasMapping = mapping.length > 0; 1199 1200 final boolean resolveId = property.resolveId() && context != null; 1201 final int valuesCount = array.length; 1202 1203 for (int j = 0; j < valuesCount; j++) { 1204 String name; 1205 String value = null; 1206 1207 final int intValue = array[j]; 1208 1209 name = String.valueOf(j); 1210 if (hasIndexMapping) { 1211 int mappingCount = indexMapping.length; 1212 for (int k = 0; k < mappingCount; k++) { 1213 final IntToString mapped = indexMapping[k]; 1214 if (mapped.from() == j) { 1215 name = mapped.to(); 1216 break; 1217 } 1218 } 1219 } 1220 1221 if (hasMapping) { 1222 int mappingCount = mapping.length; 1223 for (int k = 0; k < mappingCount; k++) { 1224 final IntToString mapped = mapping[k]; 1225 if (mapped.from() == intValue) { 1226 value = mapped.to(); 1227 break; 1228 } 1229 } 1230 } 1231 1232 if (resolveId) { 1233 if (value == null) value = (String) resolveId(context, intValue); 1234 } else { 1235 value = String.valueOf(intValue); 1236 } 1237 1238 writeEntry(out, prefix, name, suffix, value); 1239 } 1240 } 1241 1242 static Object resolveId(Context context, int id) { 1243 Object fieldValue; 1244 final Resources resources = context.getResources(); 1245 if (id >= 0) { 1246 try { 1247 fieldValue = resources.getResourceTypeName(id) + '/' + 1248 resources.getResourceEntryName(id); 1249 } catch (Resources.NotFoundException e) { 1250 fieldValue = "id/0x" + Integer.toHexString(id); 1251 } 1252 } else { 1253 fieldValue = "NO_ID"; 1254 } 1255 return fieldValue; 1256 } 1257 1258 private static void writeValue(BufferedWriter out, Object value) throws IOException { 1259 if (value != null) { 1260 String output = "[EXCEPTION]"; 1261 try { 1262 output = value.toString().replace("\n", "\\n"); 1263 } finally { 1264 out.write(String.valueOf(output.length())); 1265 out.write(","); 1266 out.write(output); 1267 } 1268 } else { 1269 out.write("4,null"); 1270 } 1271 } 1272 1273 private static Field[] capturedViewGetPropertyFields(Class<?> klass) { 1274 if (mCapturedViewFieldsForClasses == null) { 1275 mCapturedViewFieldsForClasses = new HashMap<Class<?>, Field[]>(); 1276 } 1277 final HashMap<Class<?>, Field[]> map = mCapturedViewFieldsForClasses; 1278 1279 Field[] fields = map.get(klass); 1280 if (fields != null) { 1281 return fields; 1282 } 1283 1284 final ArrayList<Field> foundFields = new ArrayList<Field>(); 1285 fields = klass.getFields(); 1286 1287 int count = fields.length; 1288 for (int i = 0; i < count; i++) { 1289 final Field field = fields[i]; 1290 if (field.isAnnotationPresent(CapturedViewProperty.class)) { 1291 field.setAccessible(true); 1292 foundFields.add(field); 1293 } 1294 } 1295 1296 fields = foundFields.toArray(new Field[foundFields.size()]); 1297 map.put(klass, fields); 1298 1299 return fields; 1300 } 1301 1302 private static Method[] capturedViewGetPropertyMethods(Class<?> klass) { 1303 if (mCapturedViewMethodsForClasses == null) { 1304 mCapturedViewMethodsForClasses = new HashMap<Class<?>, Method[]>(); 1305 } 1306 final HashMap<Class<?>, Method[]> map = mCapturedViewMethodsForClasses; 1307 1308 Method[] methods = map.get(klass); 1309 if (methods != null) { 1310 return methods; 1311 } 1312 1313 final ArrayList<Method> foundMethods = new ArrayList<Method>(); 1314 methods = klass.getMethods(); 1315 1316 int count = methods.length; 1317 for (int i = 0; i < count; i++) { 1318 final Method method = methods[i]; 1319 if (method.getParameterTypes().length == 0 && 1320 method.isAnnotationPresent(CapturedViewProperty.class) && 1321 method.getReturnType() != Void.class) { 1322 method.setAccessible(true); 1323 foundMethods.add(method); 1324 } 1325 } 1326 1327 methods = foundMethods.toArray(new Method[foundMethods.size()]); 1328 map.put(klass, methods); 1329 1330 return methods; 1331 } 1332 1333 private static String capturedViewExportMethods(Object obj, Class<?> klass, 1334 String prefix) { 1335 1336 if (obj == null) { 1337 return "null"; 1338 } 1339 1340 StringBuilder sb = new StringBuilder(); 1341 final Method[] methods = capturedViewGetPropertyMethods(klass); 1342 1343 int count = methods.length; 1344 for (int i = 0; i < count; i++) { 1345 final Method method = methods[i]; 1346 try { 1347 Object methodValue = method.invoke(obj, (Object[]) null); 1348 final Class<?> returnType = method.getReturnType(); 1349 1350 CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); 1351 if (property.retrieveReturn()) { 1352 //we are interested in the second level data only 1353 sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); 1354 } else { 1355 sb.append(prefix); 1356 sb.append(method.getName()); 1357 sb.append("()="); 1358 1359 if (methodValue != null) { 1360 final String value = methodValue.toString().replace("\n", "\\n"); 1361 sb.append(value); 1362 } else { 1363 sb.append("null"); 1364 } 1365 sb.append("; "); 1366 } 1367 } catch (IllegalAccessException e) { 1368 //Exception IllegalAccess, it is OK here 1369 //we simply ignore this method 1370 } catch (InvocationTargetException e) { 1371 //Exception InvocationTarget, it is OK here 1372 //we simply ignore this method 1373 } 1374 } 1375 return sb.toString(); 1376 } 1377 1378 private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { 1379 if (obj == null) { 1380 return "null"; 1381 } 1382 1383 StringBuilder sb = new StringBuilder(); 1384 final Field[] fields = capturedViewGetPropertyFields(klass); 1385 1386 int count = fields.length; 1387 for (int i = 0; i < count; i++) { 1388 final Field field = fields[i]; 1389 try { 1390 Object fieldValue = field.get(obj); 1391 1392 sb.append(prefix); 1393 sb.append(field.getName()); 1394 sb.append("="); 1395 1396 if (fieldValue != null) { 1397 final String value = fieldValue.toString().replace("\n", "\\n"); 1398 sb.append(value); 1399 } else { 1400 sb.append("null"); 1401 } 1402 sb.append(' '); 1403 } catch (IllegalAccessException e) { 1404 //Exception IllegalAccess, it is OK here 1405 //we simply ignore this field 1406 } 1407 } 1408 return sb.toString(); 1409 } 1410 1411 /** 1412 * Dump view info for id based instrument test generation 1413 * (and possibly further data analysis). The results are dumped 1414 * to the log. 1415 * @param tag for log 1416 * @param view for dump 1417 */ 1418 public static void dumpCapturedView(String tag, Object view) { 1419 Class<?> klass = view.getClass(); 1420 StringBuilder sb = new StringBuilder(klass.getName() + ": "); 1421 sb.append(capturedViewExportFields(view, klass, "")); 1422 sb.append(capturedViewExportMethods(view, klass, "")); 1423 Log.d(tag, sb.toString()); 1424 } 1425 1426 /** 1427 * Invoke a particular method on given view. 1428 * The given method is always invoked on the UI thread. The caller thread will stall until the 1429 * method invocation is complete. Returns an object equal to the result of the method 1430 * invocation, null if the method is declared to return void 1431 * @throws Exception if the method invocation caused any exception 1432 * @hide 1433 */ 1434 public static Object invokeViewMethod(final View view, final Method method, 1435 final Object[] args) { 1436 final CountDownLatch latch = new CountDownLatch(1); 1437 final AtomicReference<Object> result = new AtomicReference<Object>(); 1438 final AtomicReference<Throwable> exception = new AtomicReference<Throwable>(); 1439 1440 view.post(new Runnable() { 1441 @Override 1442 public void run() { 1443 try { 1444 result.set(method.invoke(view, args)); 1445 } catch (InvocationTargetException e) { 1446 exception.set(e.getCause()); 1447 } catch (Exception e) { 1448 exception.set(e); 1449 } 1450 1451 latch.countDown(); 1452 } 1453 }); 1454 1455 try { 1456 latch.await(); 1457 } catch (InterruptedException e) { 1458 throw new RuntimeException(e); 1459 } 1460 1461 if (exception.get() != null) { 1462 throw new RuntimeException(exception.get()); 1463 } 1464 1465 return result.get(); 1466 } 1467 1468 /** 1469 * @hide 1470 */ 1471 public static void setLayoutParameter(final View view, final String param, final int value) 1472 throws NoSuchFieldException, IllegalAccessException { 1473 final ViewGroup.LayoutParams p = view.getLayoutParams(); 1474 final Field f = p.getClass().getField(param); 1475 if (f.getType() != int.class) { 1476 throw new RuntimeException("Only integer layout parameters can be set. Field " 1477 + param + " is of type " + f.getType().getSimpleName()); 1478 } 1479 1480 f.set(p, Integer.valueOf(value)); 1481 1482 view.post(new Runnable() { 1483 @Override 1484 public void run() { 1485 view.setLayoutParams(p); 1486 } 1487 }); 1488 } 1489 } 1490