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