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