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