blob: 0cf3ad7e4cf69eb4de863c15df477a17eac90934 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.util;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.Executors.createAndStartNewLooper;
import static java.util.stream.Collectors.toList;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.Trace;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnDrawListener;
import android.view.Window;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.view.ViewCaptureData.ExportedData;
import com.android.launcher3.view.ViewCaptureData.FrameData;
import com.android.launcher3.view.ViewCaptureData.ViewNode;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.zip.GZIPOutputStream;
/**
* Utility class for capturing view data every frame
*/
public class ViewCapture {
private static final String TAG = "ViewCapture";
// Number of frames to keep in memory
private static final int MEMORY_SIZE = 2000;
// Initial size of the reference pool. This is at least be 5 * total number of views in
// Launcher. This allows the first free frames avoid object allocation during view capture.
private static final int INIT_POOL_SIZE = 300;
public static final MainThreadInitializedObject<ViewCapture> INSTANCE =
new MainThreadInitializedObject<>(ViewCapture::new);
private final List<WindowListener> mListeners = new ArrayList<>();
private final Context mContext;
private final LooperExecutor mExecutor;
// Pool used for capturing view tree on the UI thread.
private ViewRef mPool = new ViewRef();
private ViewCapture(Context context) {
mContext = context;
if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
Looper looper = createAndStartNewLooper("ViewCapture",
Process.THREAD_PRIORITY_FOREGROUND);
mExecutor = new LooperExecutor(looper);
mExecutor.execute(this::initPool);
} else {
mExecutor = UI_HELPER_EXECUTOR;
}
}
@UiThread
private void addToPool(ViewRef start, ViewRef end) {
end.next = mPool;
mPool = start;
}
@WorkerThread
private void initPool() {
ViewRef start = new ViewRef();
ViewRef current = start;
for (int i = 0; i < INIT_POOL_SIZE; i++) {
current.next = new ViewRef();
current = current.next;
}
ViewRef finalCurrent = current;
MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent));
}
/**
* Attaches the ViewCapture to the provided window and returns a handle to detach the listener
*/
public SafeCloseable startCapture(Window window) {
String title = window.getAttributes().getTitle().toString();
String name = TextUtils.isEmpty(title) ? window.toString() : title;
return startCapture(window.getDecorView(), name);
}
/**
* Attaches the ViewCapture to the provided window and returns a handle to detach the listener
*/
public SafeCloseable startCapture(View view, String name) {
if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
return () -> { };
}
WindowListener listener = new WindowListener(view, name);
mExecutor.execute(() -> MAIN_EXECUTOR.execute(listener::attachToRoot));
mListeners.add(listener);
return () -> {
mListeners.remove(listener);
listener.destroy();
};
}
/**
* Dumps all the active view captures
*/
public void dump(PrintWriter writer, FileDescriptor out) {
if (!FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
return;
}
ViewIdProvider idProvider = new ViewIdProvider(mContext.getResources());
// Collect all the tasks first so that all the tasks are posted on the executor
List<Pair<String, Future<ExportedData>>> tasks = mListeners.stream()
.map(l -> Pair.create(l.name, mExecutor.submit(() -> l.dumpToProto(idProvider))))
.collect(toList());
tasks.forEach(pair -> {
writer.println();
writer.println(" ContinuousViewCapture:");
writer.println(" window " + pair.first + ":");
writer.println(" pkg:" + mContext.getPackageName());
writer.print(" data:");
writer.flush();
try (OutputStream os = new FileOutputStream(out)) {
ExportedData data = pair.second.get();
OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os,
Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP));
data.writeTo(encodedOS);
encodedOS.close();
os.flush();
} catch (Exception e) {
Log.e(TAG, "Error capturing proto", e);
}
writer.println();
writer.println("--end--");
});
}
private class WindowListener implements OnDrawListener {
private final View mRoot;
public final String name;
private final Handler mHandler;
private final ViewRef mViewRef = new ViewRef();
private int mFrameIndexBg = -1;
private final long[] mFrameTimesBg = new long[MEMORY_SIZE];
private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE];
private boolean mDestroyed = false;
WindowListener(View view, String name) {
mRoot = view;
this.name = name;
mHandler = new Handler(mExecutor.getLooper(), this::captureViewPropertiesBg);
}
@Override
public void onDraw() {
Trace.beginSection("view_capture");
captureViewTree(mRoot, mViewRef);
Message m = Message.obtain(mHandler);
m.obj = mViewRef.next;
mHandler.sendMessage(m);
Trace.endSection();
}
/**
* Captures the View property on the background thread, and transfer all the ViewRef objects
* back to the pool
*/
@WorkerThread
private boolean captureViewPropertiesBg(Message msg) {
ViewRef start = (ViewRef) msg.obj;
long time = msg.getWhen();
if (start == null) {
return false;
}
mFrameIndexBg++;
if (mFrameIndexBg >= MEMORY_SIZE) {
mFrameIndexBg = 0;
}
mFrameTimesBg[mFrameIndexBg] = time;
ViewPropertyRef recycle = mNodesBg[mFrameIndexBg];
ViewPropertyRef result = null;
ViewPropertyRef resultEnd = null;
ViewRef current = start;
ViewRef last = start;
while (current != null) {
ViewPropertyRef propertyRef = recycle;
if (propertyRef == null) {
propertyRef = new ViewPropertyRef();
} else {
recycle = recycle.next;
propertyRef.next = null;
}
propertyRef.transfer(current);
last = current;
current = current.next;
if (result == null) {
result = propertyRef;
resultEnd = result;
} else {
resultEnd.next = propertyRef;
resultEnd = propertyRef;
}
}
mNodesBg[mFrameIndexBg] = result;
ViewRef end = last;
MAIN_EXECUTOR.execute(() -> addToPool(start, end));
return true;
}
void attachToRoot() {
if (mRoot.isAttachedToWindow()) {
mRoot.getViewTreeObserver().addOnDrawListener(this);
} else {
mRoot.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (!mDestroyed) {
mRoot.getViewTreeObserver().addOnDrawListener(WindowListener.this);
}
mRoot.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) { }
});
}
}
void destroy() {
mRoot.getViewTreeObserver().removeOnDrawListener(this);
mDestroyed = true;
}
@WorkerThread
private ExportedData dumpToProto(ViewIdProvider idProvider) {
ExportedData.Builder dataBuilder = ExportedData.newBuilder();
ArrayList<Class> classList = new ArrayList<>();
int size = (mNodesBg[MEMORY_SIZE - 1] == null) ? mFrameIndexBg + 1 : MEMORY_SIZE;
for (int i = size - 1; i >= 0; i--) {
int index = (MEMORY_SIZE + mFrameIndexBg - i) % MEMORY_SIZE;
ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
dataBuilder.addFrameData(FrameData.newBuilder()
.setNode(nodeBuilder)
.setTimestamp(mFrameTimesBg[index]));
}
return dataBuilder
.addAllClassname(classList.stream().map(Class::getName).collect(toList()))
.build();
}
private ViewRef captureViewTree(View view, ViewRef start) {
ViewRef ref;
if (mPool != null) {
ref = mPool;
mPool = mPool.next;
ref.next = null;
} else {
ref = new ViewRef();
}
ref.view = view;
start.next = ref;
if (view instanceof ViewGroup) {
ViewRef result = ref;
ViewGroup parent = (ViewGroup) view;
int childCount = ref.childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
result = captureViewTree(parent.getChildAt(i), result);
}
return result;
} else {
ref.childCount = 0;
return ref;
}
}
}
private static class ViewPropertyRef {
// We store reference in memory to avoid generating and storing too many strings
public Class clazz;
public int hashCode;
public int childCount = 0;
public int id;
public int left, top, right, bottom;
public int scrollX, scrollY;
public float translateX, translateY;
public float scaleX, scaleY;
public float alpha;
public float elevation;
public int visibility;
public boolean willNotDraw;
public boolean clipChildren;
public ViewPropertyRef next;
public void transfer(ViewRef viewRef) {
childCount = viewRef.childCount;
View view = viewRef.view;
viewRef.view = null;
clazz = view.getClass();
hashCode = view.hashCode();
id = view.getId();
left = view.getLeft();
top = view.getTop();
right = view.getRight();
bottom = view.getBottom();
scrollX = view.getScrollX();
scrollY = view.getScrollY();
translateX = view.getTranslationX();
translateY = view.getTranslationY();
scaleX = view.getScaleX();
scaleY = view.getScaleY();
alpha = view.getAlpha();
visibility = view.getVisibility();
willNotDraw = view.willNotDraw();
elevation = view.getElevation();
}
/**
* Converts the data to the proto representation and returns the next property ref
* at the end of the iteration.
* @return
*/
public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
ViewNode.Builder outBuilder) {
int classnameIndex = classList.indexOf(clazz);
if (classnameIndex < 0) {
classnameIndex = classList.size();
classList.add(clazz);
}
outBuilder
.setClassnameIndex(classnameIndex)
.setHashcode(hashCode)
.setId(idProvider.getName(id))
.setLeft(left)
.setTop(top)
.setWidth(right - left)
.setHeight(bottom - top)
.setTranslationX(translateX)
.setTranslationY(translateY)
.setScaleX(scaleX)
.setScaleY(scaleY)
.setAlpha(alpha)
.setVisibility(visibility)
.setWillNotDraw(willNotDraw)
.setElevation(elevation)
.setClipChildren(clipChildren);
ViewPropertyRef result = next;
for (int i = 0; (i < childCount) && (result != null); i++) {
ViewNode.Builder childBuilder = ViewNode.newBuilder();
result = result.toProto(idProvider, classList, childBuilder);
outBuilder.addChildren(childBuilder);
}
return result;
}
}
private static class ViewRef {
public View view;
public int childCount = 0;
public ViewRef next;
}
private static final class ViewIdProvider {
private final SparseArray<String> mNames = new SparseArray<>();
private final Resources mRes;
ViewIdProvider(Resources res) {
mRes = res;
}
String getName(int id) {
String name = mNames.get(id);
if (name == null) {
if (id >= 0) {
try {
name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id);
} catch (Resources.NotFoundException e) {
name = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
}
} else {
name = "NO_ID";
}
mNames.put(id, name);
}
return name;
}
}
}