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