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