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