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