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