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