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