1 /* 2 * Copyright (C) 2022 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 com.android.app.viewcapture; 18 19 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_H; 20 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_L; 21 22 import android.content.ComponentCallbacks2; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.media.permission.SafeCloseable; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.Looper; 30 import android.os.SystemClock; 31 import android.os.Trace; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.ViewTreeObserver; 38 import android.view.Window; 39 40 import androidx.annotation.AnyThread; 41 import androidx.annotation.NonNull; 42 import androidx.annotation.Nullable; 43 import androidx.annotation.UiThread; 44 import androidx.annotation.VisibleForTesting; 45 import androidx.annotation.WorkerThread; 46 47 import com.android.app.viewcapture.data.ExportedData; 48 import com.android.app.viewcapture.data.FrameData; 49 import com.android.app.viewcapture.data.MotionWindowData; 50 import com.android.app.viewcapture.data.ViewNode; 51 import com.android.app.viewcapture.data.WindowData; 52 53 import java.io.DataOutputStream; 54 import java.io.IOException; 55 import java.io.OutputStream; 56 import java.util.ArrayList; 57 import java.util.Collections; 58 import java.util.List; 59 import java.util.Optional; 60 import java.util.concurrent.CompletableFuture; 61 import java.util.concurrent.ExecutionException; 62 import java.util.concurrent.Executor; 63 import java.util.concurrent.TimeUnit; 64 import java.util.function.Consumer; 65 import java.util.function.Predicate; 66 import java.util.stream.Collectors; 67 68 /** 69 * Utility class for capturing view data every frame 70 */ 71 public abstract class ViewCapture { 72 73 private static final String TAG = "ViewCapture"; 74 75 // These flags are copies of two private flags in the View class. 76 private static final int PFLAG_INVALIDATED = 0x80000000; 77 private static final int PFLAG_DIRTY_MASK = 0x00200000; 78 79 private static final long MAGIC_NUMBER_FOR_WINSCOPE = 80 ((long) MAGIC_NUMBER_H.getNumber() << 32) | MAGIC_NUMBER_L.getNumber(); 81 82 // Number of frames to keep in memory 83 private final int mMemorySize; 84 85 // Number of ViewPropertyRef to preallocate per window 86 private final int mInitPoolSize; 87 88 protected static final int DEFAULT_MEMORY_SIZE = 2000; 89 // Initial size of the reference pool. This is at least be 5 * total number of views in 90 // Launcher. This allows the first free frames avoid object allocation during view capture. 91 protected static final int DEFAULT_INIT_POOL_SIZE = 300; 92 93 public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper()); 94 95 private final List<WindowListener> mListeners = Collections.synchronizedList(new ArrayList<>()); 96 97 protected final Executor mBgExecutor; 98 99 private boolean mIsEnabled = true; 100 101 @VisibleForTesting 102 public boolean mIsStarted = false; 103 ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor)104 protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) { 105 mMemorySize = memorySize; 106 mBgExecutor = bgExecutor; 107 mInitPoolSize = initPoolSize; 108 } 109 createAndStartNewLooperExecutor(String name, int priority)110 public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) { 111 HandlerThread thread = new HandlerThread(name, priority); 112 thread.start(); 113 return new LooperExecutor(thread.getLooper()); 114 } 115 116 /** 117 * Attaches the ViewCapture to the provided window and returns a handle to detach the listener 118 */ 119 @AnyThread 120 @NonNull startCapture(@onNull Window window)121 public SafeCloseable startCapture(@NonNull Window window) { 122 String title = window.getAttributes().getTitle().toString(); 123 String name = TextUtils.isEmpty(title) ? window.toString() : title; 124 return startCapture(window.getDecorView(), name); 125 } 126 127 /** 128 * Attaches the ViewCapture to the provided window and returns a handle to detach the listener. 129 * Verifies that ViewCapture is enabled before actually attaching an onDrawListener. 130 */ 131 @AnyThread 132 @NonNull startCapture(@onNull View view, @NonNull String name)133 public SafeCloseable startCapture(@NonNull View view, @NonNull String name) { 134 mIsStarted = true; 135 WindowListener listener = new WindowListener(view, name); 136 137 if (mIsEnabled) { 138 listener.attachToRoot(); 139 } 140 141 mListeners.add(listener); 142 143 view.getContext().registerComponentCallbacks(listener); 144 145 return () -> { 146 if (listener.mRoot != null && listener.mRoot.getContext() != null) { 147 listener.mRoot.getContext().unregisterComponentCallbacks(listener); 148 } 149 mListeners.remove(listener); 150 151 listener.detachFromRoot(); 152 }; 153 } 154 155 /** 156 * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners 157 * appear to have leaks because they store mRoot views. In reality, attached views close their 158 * respective window listeners when they are destroyed. 159 * <p> 160 * This method deletes detaches and deletes mRoot views from windowListeners. This makes the 161 * WindowListeners unusable for anything except dumping previously captured information. They 162 * are still technically enabled to allow for dumping. 163 */ 164 @VisibleForTesting 165 @AnyThread 166 public void stopCapture(@NonNull View rootView) { 167 mIsStarted = false; 168 mListeners.forEach(it -> { 169 if (rootView == it.mRoot) { 170 runOnUiThread(() -> { 171 if (it.mRoot != null) { 172 it.mRoot.getViewTreeObserver().removeOnDrawListener(it); 173 it.mRoot = null; 174 } 175 }, it.mRoot); 176 } 177 }); 178 } 179 180 @AnyThread 181 protected void enableOrDisableWindowListeners(boolean isEnabled) { 182 mIsEnabled = isEnabled; 183 mListeners.forEach(WindowListener::detachFromRoot); 184 if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot); 185 } 186 187 @AnyThread 188 protected void dumpTo(OutputStream os, Context context) 189 throws InterruptedException, ExecutionException, IOException { 190 if (mIsEnabled) { 191 DataOutputStream dataOutputStream = new DataOutputStream(os); 192 ExportedData ex = getExportedData(context); 193 dataOutputStream.writeInt(ex.getSerializedSize()); 194 ex.writeTo(dataOutputStream); 195 } 196 } 197 198 @VisibleForTesting 199 public ExportedData getExportedData(Context context) 200 throws InterruptedException, ExecutionException { 201 ArrayList<Class> classList = new ArrayList<>(); 202 return ExportedData.newBuilder() 203 .setMagicNumber(MAGIC_NUMBER_FOR_WINSCOPE) 204 .setPackage(context.getPackageName()) 205 .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get()) 206 .addAllClassname(toStringList(classList)) 207 .setRealToElapsedTimeOffsetNanos(TimeUnit.MILLISECONDS 208 .toNanos(System.currentTimeMillis()) - SystemClock.elapsedRealtimeNanos()) 209 .build(); 210 } 211 212 private static List<String> toStringList(List<Class> classList) { 213 return classList.stream().map(Class::getName).collect(Collectors.toList()); 214 } 215 216 public CompletableFuture<Optional<MotionWindowData>> getDumpTask(View view) { 217 ArrayList<Class> classList = new ArrayList<>(); 218 return getWindowData(view.getContext().getApplicationContext(), classList, 219 l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w -> 220 MotionWindowData.newBuilder() 221 .addAllFrameData(w.getFrameDataList()) 222 .addAllClassname(toStringList(classList)) 223 .build())); 224 } 225 226 @AnyThread 227 private CompletableFuture<List<WindowData>> getWindowData(Context context, 228 ArrayList<Class> outClassList, Predicate<WindowListener> filter) { 229 ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); 230 return CompletableFuture.supplyAsync( 231 () -> mListeners.stream() 232 .filter(filter) 233 .collect(Collectors.toList()), 234 MAIN_EXECUTOR).thenApplyAsync( 235 it -> it.stream() 236 .map(l -> l.dumpToProto(idProvider, outClassList)) 237 .collect(Collectors.toList()), 238 mBgExecutor); 239 } 240 241 @WorkerThread 242 protected void onCapturedViewPropertiesBg(long elapsedRealtimeNanos, String windowName, 243 ViewPropertyRef startFlattenedViewTree) { 244 } 245 246 @AnyThread 247 void runOnUiThread(Runnable action, View view) { 248 if (view == null) { 249 // Corner case. E.g.: the capture is stopped (root view set to null), 250 // but the bg thread is still processing work. 251 Log.i(TAG, "Skipping run on UI thread. Provided view == null."); 252 return; 253 } 254 255 Handler handlerUi = view.getHandler(); 256 if (handlerUi != null && handlerUi.getLooper().getThread() == Thread.currentThread()) { 257 action.run(); 258 return; 259 } 260 261 view.post(action); 262 } 263 264 /** 265 * Once this window listener is attached to a window's root view, it traverses the entire 266 * view tree on the main thread every time onDraw is called. It then saves the state of the view 267 * tree traversed in a local list of nodes, so that this list of nodes can be processed on a 268 * background thread, and prepared for being dumped into a bugreport. 269 * <p> 270 * Since some of the work needs to be done on the main thread after every draw, this piece of 271 * code needs to be hyper optimized. That is why we are recycling ViewPropertyRef objects 272 * and storing the list of nodes as a flat LinkedList, rather than as a tree. This data 273 * structure allows recycling to happen in O(1) time via pointer assignment. Without this 274 * optimization, a lot of time is wasted creating ViewPropertyRef objects, or finding 275 * ViewPropertyRef objects to recycle. 276 * <p> 277 * Another optimization is to only traverse view nodes on the main thread that have potentially 278 * changed since the last frame was drawn. This can be determined via a combination of private 279 * flags inside the View class. 280 * <p> 281 * Another optimization is to not store or manipulate any string objects on the main thread. 282 * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the 283 * main thread for up to an additional 6-7ms. It must be avoided at all costs. 284 * <p> 285 * Another optimization is to only store the class names of the Views in the view hierarchy one 286 * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef. 287 * <p> 288 * TODO: b/262585897: If further memory optimization is required, an effective one would be to 289 * only store the changes between frames, rather than the entire node tree for each frame. 290 * The go/web-hv UX already does this, and has reaped significant memory improves because of it. 291 * <p> 292 * TODO: b/262585897: Another memory optimization could be to store all integer, float, and 293 * boolean information via single integer values via the Chinese remainder theorem, or a similar 294 * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this 295 * would allow each ViewPropertyRef to slim down its memory footprint significantly. 296 * <p> 297 * One important thing to remember is that bugs related to recycling will usually only appear 298 * after at least 2000 frames have been rendered. If that code is changed, the tester can 299 * use hard-coded logs to verify that recycling is happening, and test view capturing at least 300 * ~8000 frames or so to verify the recycling functionality is working properly. 301 * <p> 302 * Each WindowListener is memory aware and will both stop collecting view capture information, 303 * as well as delete their current stash of information upon a signal from the system that 304 * memory resources are scarce. The user will need to restart the app process before 305 * more ViewCapture information is captured. 306 */ 307 private class WindowListener implements ViewTreeObserver.OnDrawListener, ComponentCallbacks2 { 308 309 @Nullable 310 public View mRoot; 311 public final String name; 312 313 // Pool used for capturing view tree on the UI thread. 314 private ViewPropertyRef mPool = new ViewPropertyRef(); 315 private final ViewPropertyRef mViewPropertyRef = new ViewPropertyRef(); 316 317 private int mFrameIndexBg = -1; 318 private boolean mIsFirstFrame = true; 319 private long[] mFrameTimesNanosBg = new long[mMemorySize]; 320 private ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize]; 321 322 private boolean mIsActive = true; 323 private final Consumer<ViewPropertyRef> mCaptureCallback = 324 this::copyCleanViewsFromLastFrameBg; 325 326 WindowListener(View view, String name) { 327 mRoot = view; 328 this.name = name; 329 initPool(mInitPoolSize); 330 } 331 332 /** 333 * Every time onDraw is called, it does the minimal set of work required on the main thread, 334 * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the 335 * rest of the processing work (extracting the captured view properties) to a background 336 * thread via mExecutor. 337 */ 338 @Override 339 @UiThread 340 public void onDraw() { 341 Trace.beginSection("vc#onDraw"); 342 try { 343 View root = mRoot; 344 if (root == null) { 345 // Handle the corner case where another (non-UI) thread 346 // concurrently stopped the capture and set mRoot = null 347 return; 348 } 349 captureViewTree(root, mViewPropertyRef); 350 ViewPropertyRef captured = mViewPropertyRef.next; 351 if (captured != null) { 352 captured.callback = mCaptureCallback; 353 captured.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); 354 mBgExecutor.execute(captured); 355 } 356 mIsFirstFrame = false; 357 } finally { 358 Trace.endSection(); 359 } 360 } 361 362 /** 363 * Copy clean views from the last frame on the background thread. Clean views are 364 * the remaining part of the view hierarchy that was not already copied by the UI thread. 365 * Then transfer the received ViewPropertyRef objects back to the UI thread's pool. 366 */ 367 @WorkerThread 368 private void copyCleanViewsFromLastFrameBg(ViewPropertyRef start) { 369 Trace.beginSection("vc#copyCleanViewsFromLastFrameBg"); 370 371 long elapsedRealtimeNanos = start.elapsedRealtimeNanos; 372 mFrameIndexBg++; 373 if (mFrameIndexBg >= mMemorySize) { 374 mFrameIndexBg = 0; 375 } 376 mFrameTimesNanosBg[mFrameIndexBg] = elapsedRealtimeNanos; 377 378 ViewPropertyRef recycle = mNodesBg[mFrameIndexBg]; 379 380 ViewPropertyRef resultStart = null; 381 ViewPropertyRef resultEnd = null; 382 383 ViewPropertyRef end = start; 384 385 while (end != null) { 386 end.completeTransferFromViewBg(); 387 388 ViewPropertyRef propertyRef = recycle; 389 if (propertyRef == null) { 390 propertyRef = new ViewPropertyRef(); 391 } else { 392 recycle = recycle.next; 393 propertyRef.next = null; 394 } 395 396 ViewPropertyRef copy = null; 397 if (end.childCount < 0) { 398 copy = findInLastFrame(end.hashCode); 399 if (copy != null) { 400 copy.transferTo(end); 401 } else { 402 end.childCount = 0; 403 } 404 } 405 end.transferTo(propertyRef); 406 407 if (resultStart == null) { 408 resultStart = propertyRef; 409 resultEnd = resultStart; 410 } else { 411 resultEnd.next = propertyRef; 412 resultEnd = resultEnd.next; 413 } 414 415 if (copy != null) { 416 int pending = copy.childCount; 417 while (pending > 0) { 418 copy = copy.next; 419 pending = pending - 1 + copy.childCount; 420 421 propertyRef = recycle; 422 if (propertyRef == null) { 423 propertyRef = new ViewPropertyRef(); 424 } else { 425 recycle = recycle.next; 426 propertyRef.next = null; 427 } 428 429 copy.transferTo(propertyRef); 430 431 resultEnd.next = propertyRef; 432 resultEnd = resultEnd.next; 433 } 434 } 435 436 if (end.next == null) { 437 // The compiler will complain about using a non-final variable from 438 // an outer class in a lambda if we pass in 'end' directly. 439 final ViewPropertyRef finalEnd = end; 440 runOnUiThread(() -> addToPool(start, finalEnd), mRoot); 441 break; 442 } 443 end = end.next; 444 } 445 mNodesBg[mFrameIndexBg] = resultStart; 446 447 onCapturedViewPropertiesBg(elapsedRealtimeNanos, name, resultStart); 448 449 Trace.endSection(); 450 } 451 452 @WorkerThread 453 private @Nullable ViewPropertyRef findInLastFrame(int hashCode) { 454 int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1; 455 ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex]; 456 while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) { 457 viewPropertyRef = viewPropertyRef.next; 458 } 459 return viewPropertyRef; 460 } 461 462 private void initPool(int initPoolSize) { 463 ViewPropertyRef start = new ViewPropertyRef(); 464 ViewPropertyRef current = start; 465 466 for (int i = 0; i < initPoolSize; i++) { 467 current.next = new ViewPropertyRef(); 468 current = current.next; 469 } 470 471 ViewPropertyRef finalCurrent = current; 472 addToPool(start, finalCurrent); 473 } 474 475 private void addToPool(ViewPropertyRef start, ViewPropertyRef end) { 476 end.next = mPool; 477 mPool = start; 478 } 479 480 @UiThread 481 private ViewPropertyRef getFromPool() { 482 ViewPropertyRef ref = mPool; 483 if (ref != null) { 484 mPool = ref.next; 485 ref.next = null; 486 } else { 487 ref = new ViewPropertyRef(); 488 } 489 return ref; 490 } 491 492 @AnyThread 493 void attachToRoot() { 494 if (mRoot == null) return; 495 mIsActive = true; 496 runOnUiThread(() -> { 497 if (mRoot.isAttachedToWindow()) { 498 safelyEnableOnDrawListener(); 499 } else { 500 mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { 501 @Override 502 public void onViewAttachedToWindow(View v) { 503 if (mIsActive) { 504 safelyEnableOnDrawListener(); 505 } 506 mRoot.removeOnAttachStateChangeListener(this); 507 } 508 509 @Override 510 public void onViewDetachedFromWindow(View v) { 511 } 512 }); 513 } 514 }, mRoot); 515 } 516 517 @AnyThread 518 void detachFromRoot() { 519 mIsActive = false; 520 runOnUiThread(() -> { 521 if (mRoot != null) { 522 mRoot.getViewTreeObserver().removeOnDrawListener(this); 523 } 524 }, mRoot); 525 } 526 527 @UiThread 528 private void safelyEnableOnDrawListener() { 529 if (mRoot != null) { 530 mRoot.getViewTreeObserver().removeOnDrawListener(this); 531 mRoot.getViewTreeObserver().addOnDrawListener(this); 532 } 533 } 534 535 @WorkerThread 536 private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) { 537 WindowData.Builder builder = WindowData.newBuilder().setTitle(name); 538 int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize; 539 for (int i = size - 1; i >= 0; i--) { 540 int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize; 541 ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); 542 mNodesBg[index].toProto(idProvider, classList, nodeBuilder); 543 FrameData.Builder frameDataBuilder = FrameData.newBuilder() 544 .setNode(nodeBuilder) 545 .setTimestamp(mFrameTimesNanosBg[index]); 546 builder.addFrameData(frameDataBuilder); 547 } 548 return builder.build(); 549 } 550 551 @UiThread 552 private ViewPropertyRef captureViewTree(View view, ViewPropertyRef start) { 553 ViewPropertyRef ref = getFromPool(); 554 start.next = ref; 555 if (view instanceof ViewGroup) { 556 ViewGroup parent = (ViewGroup) view; 557 // If a view has not changed since the last frame, we will copy 558 // its children from the last processed frame's data. 559 if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0 560 && !mIsFirstFrame) { 561 // A negative child count is the signal to copy this view from the last frame. 562 ref.childCount = -1; 563 ref.view = view; 564 return ref; 565 } 566 ViewPropertyRef result = ref; 567 int childCount = ref.childCount = parent.getChildCount(); 568 ref.transferFrom(view); 569 for (int i = 0; i < childCount; i++) { 570 result = captureViewTree(parent.getChildAt(i), result); 571 } 572 return result; 573 } else { 574 ref.childCount = 0; 575 ref.transferFrom(view); 576 return ref; 577 } 578 } 579 580 @Override 581 public void onTrimMemory(int level) { 582 if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { 583 mNodesBg = new ViewPropertyRef[0]; 584 mFrameTimesNanosBg = new long[0]; 585 if (mRoot != null && mRoot.getContext() != null) { 586 mRoot.getContext().unregisterComponentCallbacks(this); 587 } 588 detachFromRoot(); 589 mRoot = null; 590 } 591 } 592 593 @Override 594 public void onConfigurationChanged(Configuration configuration) { 595 // No Operation 596 } 597 598 @Override 599 public void onLowMemory() { 600 onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); 601 } 602 } 603 604 protected static class ViewPropertyRef implements Runnable { 605 public View view; 606 607 // We store reference in memory to avoid generating and storing too many strings 608 public Class clazz; 609 public int hashCode; 610 611 public int id; 612 public int left, top, right, bottom; 613 public int scrollX, scrollY; 614 615 public float translateX, translateY; 616 public float scaleX, scaleY; 617 public float alpha; 618 public float elevation; 619 620 public int visibility; 621 public boolean willNotDraw; 622 public boolean clipChildren; 623 public int childCount = 0; 624 625 public ViewPropertyRef next; 626 627 public Consumer<ViewPropertyRef> callback = null; 628 public long elapsedRealtimeNanos = 0; 629 630 631 public void transferFrom(View in) { 632 view = in; 633 634 left = in.getLeft(); 635 top = in.getTop(); 636 right = in.getRight(); 637 bottom = in.getBottom(); 638 scrollX = in.getScrollX(); 639 scrollY = in.getScrollY(); 640 641 translateX = in.getTranslationX(); 642 translateY = in.getTranslationY(); 643 scaleX = in.getScaleX(); 644 scaleY = in.getScaleY(); 645 alpha = in.getAlpha(); 646 elevation = in.getElevation(); 647 648 visibility = in.getVisibility(); 649 willNotDraw = in.willNotDraw(); 650 } 651 652 /** 653 * Transfer in backgroup thread view properties that remain unchanged between frames. 654 */ 655 public void completeTransferFromViewBg() { 656 clazz = view.getClass(); 657 hashCode = view.hashCode(); 658 id = view.getId(); 659 view = null; 660 } 661 662 public void transferTo(ViewPropertyRef out) { 663 out.clazz = this.clazz; 664 out.hashCode = this.hashCode; 665 out.childCount = this.childCount; 666 out.id = this.id; 667 out.left = this.left; 668 out.top = this.top; 669 out.right = this.right; 670 out.bottom = this.bottom; 671 out.scrollX = this.scrollX; 672 out.scrollY = this.scrollY; 673 out.scaleX = this.scaleX; 674 out.scaleY = this.scaleY; 675 out.translateX = this.translateX; 676 out.translateY = this.translateY; 677 out.alpha = this.alpha; 678 out.visibility = this.visibility; 679 out.willNotDraw = this.willNotDraw; 680 out.clipChildren = this.clipChildren; 681 out.elevation = this.elevation; 682 } 683 684 /** 685 * Converts the data to the proto representation and returns the next property ref 686 * at the end of the iteration. 687 */ 688 public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList, 689 ViewNode.Builder viewNode) { 690 int classnameIndex = classList.indexOf(clazz); 691 if (classnameIndex < 0) { 692 classnameIndex = classList.size(); 693 classList.add(clazz); 694 } 695 696 viewNode.setClassnameIndex(classnameIndex) 697 .setHashcode(hashCode) 698 .setId(idProvider.getName(id)) 699 .setLeft(left) 700 .setTop(top) 701 .setWidth(right - left) 702 .setHeight(bottom - top) 703 .setTranslationX(translateX) 704 .setTranslationY(translateY) 705 .setScrollX(scrollX) 706 .setScrollY(scrollY) 707 .setScaleX(scaleX) 708 .setScaleY(scaleY) 709 .setAlpha(alpha) 710 .setVisibility(visibility) 711 .setWillNotDraw(willNotDraw) 712 .setElevation(elevation) 713 .setClipChildren(clipChildren); 714 715 ViewPropertyRef result = next; 716 for (int i = 0; (i < childCount) && (result != null); i++) { 717 ViewNode.Builder childViewNode = ViewNode.newBuilder(); 718 result = result.toProto(idProvider, classList, childViewNode); 719 viewNode.addChildren(childViewNode); 720 } 721 return result; 722 } 723 724 @Override 725 public void run() { 726 Consumer<ViewPropertyRef> oldCallback = callback; 727 callback = null; 728 if (oldCallback != null) { 729 oldCallback.accept(this); 730 } 731 } 732 } 733 734 protected static final class ViewIdProvider { 735 736 private final SparseArray<String> mNames = new SparseArray<>(); 737 private final Resources mRes; 738 739 ViewIdProvider(Resources res) { 740 mRes = res; 741 } 742 743 String getName(int id) { 744 String name = mNames.get(id); 745 if (name == null) { 746 if (id >= 0) { 747 try { 748 name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id); 749 } catch (Resources.NotFoundException e) { 750 name = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); 751 } 752 } else { 753 name = "NO_ID"; 754 } 755 mNames.put(id, name); 756 } 757 return name; 758 } 759 } 760 } 761