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