Merge Android 14 QPR2 to AOSP main
Bug: 319669529
Merged-In: I05afba0a8cd4da7bf28b5b23929a82ff77764fea
Change-Id: I5150a77097411a950f187ceebd707c461c936178
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 60ec99d..d38a961 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,3 +6,9 @@
[Hook Scripts]
winscope = ./tools/winscope/hooks/pre-upload ${PREUPLOAD_FILES}
+
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+
+ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check -i ${REPO_ROOT}/frameworks/base/ktfmt_includes.txt ${PREUPLOAD_FILES}
+
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
\ No newline at end of file
diff --git a/apps/Development/AndroidManifest.xml b/apps/Development/AndroidManifest.xml
index 016a245..7310f1c 100644
--- a/apps/Development/AndroidManifest.xml
+++ b/apps/Development/AndroidManifest.xml
@@ -43,9 +43,9 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.googleapps.permission.ACCESS_GOOGLE_PASSWORD" />
- <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
- <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.ALL_SERVICES" />
- <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.YouTubeUser" />
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" android:maxSdkVersion="34"/>
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.ALL_SERVICES" android:maxSdkVersion="34"/>
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.YouTubeUser" android:maxSdkVersion="34"/>
<application android:label="Dev Tools"
android:icon="@mipmap/ic_launcher_devtools">
diff --git a/apps/PushApiAuthenticator/Android.bp b/apps/PushApiAuthenticator/Android.bp
index 48ffea0..1d45d79 100644
--- a/apps/PushApiAuthenticator/Android.bp
+++ b/apps/PushApiAuthenticator/Android.bp
@@ -11,7 +11,9 @@
name: "PushApiAuthenticator",
// Only compile source java files in this apk.
srcs: ["src/**/*.java"],
- sdk_version: "current",
+ sdk_version: "26",
+ target_sdk_version: "26",
+ min_sdk_version: "26",
dex_preopt: {
enabled: false,
},
diff --git a/apps/PushApiAuthenticator/AndroidManifest.xml b/apps/PushApiAuthenticator/AndroidManifest.xml
index 2b641d1..d89aa8b 100644
--- a/apps/PushApiAuthenticator/AndroidManifest.xml
+++ b/apps/PushApiAuthenticator/AndroidManifest.xml
@@ -19,9 +19,10 @@
package="com.example.android.pushapiauthenticator">
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1"
- android:targetSdkVersion="26"/>
+ android:targetSdkVersion="30"/>
<application android:label="Auth Tester" android:icon="@drawable/push">
<activity android:name="MainActivity">
<intent-filter>
diff --git a/apps/PushApiAuthenticator/res/layout/activity_main.xml b/apps/PushApiAuthenticator/res/layout/activity_main.xml
index 715d90e..3199e4d 100644
--- a/apps/PushApiAuthenticator/res/layout/activity_main.xml
+++ b/apps/PushApiAuthenticator/res/layout/activity_main.xml
@@ -171,14 +171,14 @@
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="SET UM_NOT_VISIBLE(4)"
+ android:text="SET UM_NOT_VISIBLE (4)"
android:id="@+id/notVisibleButton"
android:checked="false" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="SET NOT_VISIBLE(3)"
+ android:text="SET NOT_VISIBLE (3)"
android:id="@+id/forcedNotVisibleButton"
android:checked="false" />
diff --git a/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java b/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
index 9745823..826d844 100644
--- a/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
+++ b/apps/PushApiAuthenticator/src/com/example/android/pushapiauthenticator/MainActivity.java
@@ -161,7 +161,7 @@
am.setAccountVisibility(currentAccount, packageName,
AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
Toast.makeText(
- getApplicationContext(), "Set UM_VISIBLE(2) "
+ getApplicationContext(), "Set UM_VISIBLE (2) "
+ currentAccount.name + " to " + packageName,
Toast.LENGTH_SHORT).show();
break;
@@ -169,7 +169,7 @@
am.setAccountVisibility(currentAccount, packageName,
AccountManager.VISIBILITY_USER_MANAGED_NOT_VISIBLE);
Toast.makeText(
- getApplicationContext(), "Set UM_NOT_VISIBLE(4) "
+ getApplicationContext(), "Set UM_NOT_VISIBLE (4) "
+ currentAccount.name + " to " + packageName,
Toast.LENGTH_SHORT).show();
break;
@@ -177,20 +177,21 @@
am.setAccountVisibility(currentAccount, packageName,
AccountManager.VISIBILITY_NOT_VISIBLE);
Toast.makeText(
- getApplicationContext(), "Removing visibility(3) "
+ getApplicationContext(), "Removing visibility (3) "
+ currentAccount.name + " of " + packageName,
Toast.LENGTH_SHORT).show();
break;
case R.id.getButton:
Toast.makeText(getApplicationContext(),
- "Is " + currentAccount.name + " visible to " + packageName
- + "?\n"
- + am.getAccountVisibility(currentAccount, packageName),
+ "Visibility = "
+ + am.getAccountVisibility(currentAccount, packageName)
+ + " for " + currentAccount.name + ", app= "
+ + packageName,
Toast.LENGTH_SHORT).show();
break;
case R.id.addAccountButton:
Toast.makeText(getApplicationContext(),
- "Adding account explicitly!"
+ "Adding account "
+ am.addAccountExplicitly(currentAccount, null, null),
Toast.LENGTH_SHORT).show();
break;
@@ -199,7 +200,7 @@
packageAndVisibilitys.put(packageName,
AccountManager.VISIBILITY_USER_MANAGED_VISIBLE);
Toast.makeText(getApplicationContext(),
- "Adding account explicitly!"
+ "Adding account "
+ am.addAccountExplicitly(currentAccount, null, null,
packageAndVisibilitys)
+ " with visibility for " + packageName + "!",
@@ -207,7 +208,7 @@
break;
case R.id.removeAccount:
Toast.makeText(getApplicationContext(),
- "Removing account explicitly!"
+ "Removing account "
+ am.removeAccountExplicitly(currentAccount),
Toast.LENGTH_SHORT).show();
break;
diff --git a/cmds/monkey/Android.bp b/cmds/monkey/Android.bp
index 5a873f1..85e12c6 100644
--- a/cmds/monkey/Android.bp
+++ b/cmds/monkey/Android.bp
@@ -12,3 +12,29 @@
srcs: ["**/*.java"],
wrapper: "monkey.sh",
}
+
+android_test {
+ // This test does not need to run on device. It's a regular Java unit test. But it needs to
+ // access some framework code like MotionEvent, KeyEvent, InputDevice, etc, which is currently
+ // not available for the host.
+ // Therefore, we are relying on 'android_test' here until ravenwood is ready.
+ name: "monkey_test",
+ srcs: ["**/*.java",
+ "**/*.kt",
+ ],
+
+ kotlincflags: [
+ "-Werror",
+ ],
+
+ static_libs: [
+ "androidx.test.runner",
+ ],
+
+ libs: [
+ "junit",
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+}
diff --git a/cmds/monkey/AndroidManifest.xml b/cmds/monkey/AndroidManifest.xml
new file mode 100644
index 0000000..b5ccd01
--- /dev/null
+++ b/cmds/monkey/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.commands.monkey">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.commands.monkey"
+ android:label="Monkey Test"/>
+</manifest>
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
index c650aee..d56e5ec 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionEvent.java
@@ -19,6 +19,7 @@
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.IActivityManager;
+import android.content.Context;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
@@ -49,10 +50,11 @@
Logger.out.println(String.format(":Permission %s %s to package %s",
grant ? "grant" : "revoke", mPermissionInfo.name, mPkg));
if (grant) {
- permissionManager.grantRuntimePermission(mPkg, mPermissionInfo.name, currentUser);
+ permissionManager.grantRuntimePermission(mPkg, mPermissionInfo.name,
+ Context.DEVICE_ID_DEFAULT, currentUser);
} else {
- permissionManager.revokeRuntimePermission(mPkg, mPermissionInfo.name, currentUser,
- null);
+ permissionManager.revokeRuntimePermission(mPkg, mPermissionInfo.name,
+ Context.DEVICE_ID_DEFAULT, currentUser, null);
}
return MonkeyEvent.INJECT_SUCCESS;
} catch (RemoteException re) {
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
index 21be743..b6f9e75 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyPermissionUtil.java
@@ -17,6 +17,7 @@
package com.android.commands.monkey;
import android.Manifest;
+import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
@@ -106,7 +107,8 @@
}
private boolean shouldTargetPermission(String pkg, PermissionInfo pi) throws RemoteException {
- int flags = mPermManager.getPermissionFlags(pkg, pi.name, UserHandle.myUserId());
+ int flags = mPermManager.getPermissionFlags(pkg, pi.name, Context.DEVICE_ID_DEFAULT,
+ UserHandle.myUserId());
int fixedPermFlags = PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
| PackageManager.FLAG_PERMISSION_POLICY_FIXED;
return pi.group != null && pi.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java b/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
index 08f66bc..ad6df5f 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeyRotationEvent.java
@@ -50,9 +50,9 @@
// inject rotation event
try {
- iwm.freezeRotation(mRotationDegree);
+ iwm.freezeRotation(mRotationDegree, /* caller= */ "MonkeyRotationEven#injectEvent");
if (!mPersist) {
- iwm.thawRotation();
+ iwm.thawRotation(/* caller= */ "MonkeyRotationEven#injectEvent");
}
return MonkeyEvent.INJECT_SUCCESS;
} catch (RemoteException ex) {
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScript.java b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScript.java
index eaa9d1c..dcbfbb4 100644
--- a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScript.java
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScript.java
@@ -580,9 +580,12 @@
y2, 1, 5));
}
eventTime = SystemClock.uptimeMillis();
- mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_UP)
+ mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_POINTER_UP
+ | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT))
.setDownTime(downTime).setEventTime(eventTime).addPointer(0, x1, y1)
.addPointer(1, x2, y2));
+ mQ.addLast(new MonkeyTouchEvent(MotionEvent.ACTION_UP)
+ .setDownTime(downTime).setEventTime(eventTime).addPointer(0, x1, y1));
}
}
diff --git a/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScriptTest.kt b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScriptTest.kt
new file mode 100644
index 0000000..f717ded
--- /dev/null
+++ b/cmds/monkey/src/com/android/commands/monkey/MonkeySourceScriptTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023 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.commands.monkey
+
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_POINTER_DOWN
+import android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT
+import android.view.MotionEvent.ACTION_POINTER_UP
+import android.view.MotionEvent.ACTION_UP
+
+import java.io.BufferedWriter
+import java.io.FileWriter
+import java.io.File
+import java.util.Random
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+
+import org.junit.Test
+
+private fun receiveEvent(script: MonkeySourceScript, type: Int): MonkeyEvent {
+ val event = script.getNextEvent()
+ assertNotNull(event)
+ assertEquals(type, event.getEventType())
+ return event
+}
+
+private fun assertTouchEvent(script: MonkeySourceScript, action: Int) {
+ val motionEvent = receiveEvent(script, MonkeyEvent.EVENT_TYPE_TOUCH) as MonkeyMotionEvent
+ assertEquals(action, motionEvent.getAction())
+}
+
+/**
+ * Test for class MonkeySourceScript
+ */
+class MonkeySourceScriptTest {
+ companion object {
+ const val ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN.or(1.shl(ACTION_POINTER_INDEX_SHIFT))
+ const val ACTION_POINTER_1_UP = ACTION_POINTER_UP.or(1.shl(ACTION_POINTER_INDEX_SHIFT))
+ }
+
+ /**
+ * Send a PinchZoom command and check the resulting event stream.
+ * Since ACTION_UP is a throttlable event, an event with TYPE_THROTTLE is expected at the end.
+ */
+ @Test
+ fun pinchZoom() {
+ val file = File.createTempFile("pinch_zoom", null)
+ val fileName = file.getAbsolutePath()
+ BufferedWriter(FileWriter(fileName)).use { writer ->
+ writer.write("start data >>\n")
+ writer.write("PinchZoom(100,100,200,200,50,50,10,10,5)\n")
+ }
+
+ val script = MonkeySourceScript(Random(), fileName, 0, false, 0, 0)
+
+ assertTouchEvent(script, ACTION_DOWN)
+ assertTouchEvent(script, ACTION_POINTER_1_DOWN)
+ assertTouchEvent(script, ACTION_MOVE)
+ assertTouchEvent(script, ACTION_MOVE)
+ assertTouchEvent(script, ACTION_MOVE)
+ assertTouchEvent(script, ACTION_MOVE)
+ assertTouchEvent(script, ACTION_MOVE)
+ assertTouchEvent(script, ACTION_POINTER_1_UP)
+ assertTouchEvent(script, ACTION_UP)
+ receiveEvent(script, MonkeyEvent.EVENT_TYPE_THROTTLE)
+ assertNull(script.getNextEvent())
+
+ file.deleteOnExit()
+ }
+}
diff --git a/ide/clion/hardware/interfaces/graphics/CMakeLists.txt b/ide/clion/hardware/interfaces/graphics/CMakeLists.txt
index fd57659..31f6b8a 100644
--- a/ide/clion/hardware/interfaces/graphics/CMakeLists.txt
+++ b/ide/clion/hardware/interfaces/graphics/CMakeLists.txt
@@ -7,7 +7,7 @@
try_add_subdir(allocator/aidl/android.hardware.graphics.allocator-V2-ndk)
try_add_subdir(composer/aidl/android.hardware.graphics.composer3-V2-ndk)
try_add_subdir(composer/aidl/vts/VtsHalGraphicsComposer3_TargetTest)
-try_add_subdir(common/aidl/android.hardware.graphics.common-V4-ndk)
+try_add_subdir(common/aidl/android.hardware.graphics.common-V5-ndk)
try_add_subdir(mapper/stable-c/libimapper_providerutils_tests)
try_add_subdir(mapper/stable-c/VtsHalGraphicsMapperStableC_TargetTest)
try_add_subdir(mapper/4.0/vts/functional/VtsHalGraphicsMapperV4_0TargetTest)
\ No newline at end of file
diff --git a/ide/intellij/codestyles/AndroidStyle.xml b/ide/intellij/codestyles/AndroidStyle.xml
index 7f7d6b2..4f9bc65 100644
--- a/ide/intellij/codestyles/AndroidStyle.xml
+++ b/ide/intellij/codestyles/AndroidStyle.xml
@@ -1,111 +1,93 @@
-<code_scheme name="AndroidStyle">
- <option name="JAVA_INDENT_OPTIONS">
- <value>
- <option name="INDENT_SIZE" value="4" />
- <option name="CONTINUATION_INDENT_SIZE" value="8" />
- <option name="TAB_SIZE" value="8" />
- <option name="USE_TAB_CHARACTER" value="false" />
- <option name="SMART_TABS" value="false" />
- <option name="LABEL_INDENT_SIZE" value="0" />
- <option name="LABEL_INDENT_ABSOLUTE" value="false" />
- <option name="USE_RELATIVE_INDENTS" value="false" />
- </value>
- </option>
- <option name="FIELD_NAME_PREFIX" value="m" />
- <option name="STATIC_FIELD_NAME_PREFIX" value="s" />
- <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
- <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
- <option name="IMPORT_LAYOUT_TABLE">
- <value>
- <package name="android" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="androidx" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="com.android" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="dalvik" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="libcore" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="com" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="gov" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="junit" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="net" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="org" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="java" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="javax" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="" withSubpackages="true" static="true" />
- <emptyLine />
- <package name="android" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="androidx" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="com.android" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="dalvik" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="libcore" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="com" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="gov" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="junit" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="net" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="org" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="java" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="javax" withSubpackages="true" static="false" />
- <emptyLine />
- <package name="" withSubpackages="true" static="false" />
- </value>
- </option>
+<code_scheme name="AndroidStyle" version="173">
<option name="RIGHT_MARGIN" value="100" />
- <option name="JD_P_AT_EMPTY_LINES" value="false" />
- <option name="JD_DO_NOT_WRAP_ONE_LINE_COMMENTS" value="true" />
- <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
- <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
- <option name="JD_KEEP_EMPTY_RETURN" value="false" />
- <option name="JD_PRESERVE_LINE_FEEDS" value="true" />
- <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
- <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
- <option name="BLANK_LINES_AROUND_FIELD" value="1" />
- <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
- <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
- <option name="ALIGN_MULTILINE_FOR" value="false" />
- <option name="CALL_PARAMETERS_WRAP" value="1" />
- <option name="METHOD_PARAMETERS_WRAP" value="1" />
- <option name="EXTENDS_LIST_WRAP" value="1" />
- <option name="THROWS_LIST_WRAP" value="1" />
- <option name="EXTENDS_KEYWORD_WRAP" value="1" />
- <option name="THROWS_KEYWORD_WRAP" value="1" />
- <option name="METHOD_CALL_CHAIN_WRAP" value="1" />
- <option name="BINARY_OPERATION_WRAP" value="1" />
- <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
- <option name="TERNARY_OPERATION_WRAP" value="1" />
- <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
- <option name="FOR_STATEMENT_WRAP" value="1" />
- <option name="ARRAY_INITIALIZER_WRAP" value="1" />
- <option name="ASSIGNMENT_WRAP" value="1" />
- <option name="PLACE_ASSIGNMENT_SIGN_ON_NEXT_LINE" value="true" />
- <option name="WRAP_COMMENTS" value="true" />
- <option name="IF_BRACE_FORCE" value="3" />
- <option name="DOWHILE_BRACE_FORCE" value="3" />
- <option name="WHILE_BRACE_FORCE" value="3" />
- <option name="FOR_BRACE_FORCE" value="3" />
- <XML>
- <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
- </XML>
+ <JavaCodeStyleSettings>
+ <option name="FIELD_NAME_PREFIX" value="m" />
+ <option name="STATIC_FIELD_NAME_PREFIX" value="s" />
+ <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
+ <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
+ <option name="IMPORT_LAYOUT_TABLE">
+ <value>
+ <package name="android" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="androidx" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="com.android" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="dalvik" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="libcore" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="com" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="gov" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="junit" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="kotlin" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="net" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="org" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="java" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="javax" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="" withSubpackages="true" static="true" />
+ <emptyLine />
+ <package name="android" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="androidx" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="com.android" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="dalvik" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="libcore" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="com" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="dagger" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="gov" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="junit" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="kotlin" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="net" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="org" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="java" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="javax" withSubpackages="true" static="false" />
+ <emptyLine />
+ <package name="" withSubpackages="true" static="false" />
+ </value>
+ </option>
+ <option name="JD_P_AT_EMPTY_LINES" value="false" />
+ <option name="JD_DO_NOT_WRAP_ONE_LINE_COMMENTS" value="true" />
+ <option name="JD_KEEP_EMPTY_PARAMETER" value="false" />
+ <option name="JD_KEEP_EMPTY_EXCEPTION" value="false" />
+ <option name="JD_KEEP_EMPTY_RETURN" value="false" />
+ <option name="JD_PRESERVE_LINE_FEEDS" value="true" />
+ </JavaCodeStyleSettings>
+ <JetCodeStyleSettings>
+ <option name="PACKAGES_IMPORT_LAYOUT">
+ <value>
+ <package name="" alias="false" withSubpackages="true" />
+ <package name="" alias="true" withSubpackages="true" />
+ </value>
+ </option>
+ <option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" />
+ <option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="false" />
+ <option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="false" />
+ <option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="false" />
+ <option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="false" />
+ <option name="CONTINUATION_INDENT_IN_ELVIS" value="false" />
+ <option name="WRAP_EXPRESSION_BODY_FUNCTIONS" value="1" />
+ </JetCodeStyleSettings>
<ADDITIONAL_INDENT_OPTIONS fileType="java">
<option name="TAB_SIZE" value="8" />
</ADDITIONAL_INDENT_OPTIONS>
@@ -129,6 +111,7 @@
<option name="FOR_STATEMENT_WRAP" value="1" />
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
+ <option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="1" />
<option name="DOWHILE_BRACE_FORCE" value="1" />
<option name="WHILE_BRACE_FORCE" value="1" />
@@ -321,4 +304,21 @@
</rules>
</arrangement>
</codeStyleSettings>
+ <codeStyleSettings language="kotlin">
+ <option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
+ <option name="CALL_PARAMETERS_WRAP" value="5" />
+ <option name="METHOD_PARAMETERS_WRAP" value="5" />
+ <option name="EXTENDS_LIST_WRAP" value="5" />
+ <option name="METHOD_CALL_CHAIN_WRAP" value="5" />
+ <option name="ASSIGNMENT_WRAP" value="5" />
+ <option name="PARAMETER_ANNOTATION_WRAP" value="5" />
+ <option name="VARIABLE_ANNOTATION_WRAP" value="5" />
+ <option name="ENUM_CONSTANTS_WRAP" value="1" />
+ </codeStyleSettings>
+ <codeStyleSettings language="prototext">
+ <indentOptions>
+ <option name="INDENT_SIZE" value="4" />
+ <option name="TAB_SIZE" value="4" />
+ </indentOptions>
+ </codeStyleSettings>
</code_scheme>
\ No newline at end of file
diff --git a/samples/AconfigDemo/aconfig_demo_flags.aconfig b/samples/AconfigDemo/aconfig_demo_flags.aconfig
index 80f4f6e..f3c5f6e 100644
--- a/samples/AconfigDemo/aconfig_demo_flags.aconfig
+++ b/samples/AconfigDemo/aconfig_demo_flags.aconfig
@@ -49,4 +49,11 @@
description: "A read only flag for demo"
bug: "298754733"
is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+ name: "test_flag_gantry"
+ namespace: "gantry"
+ description: "A flag used internally by the aconfig team for some testing. I'll be deleted soon!"
+ bug: "297503172"
+}
diff --git a/samples/ApiDemos/src/com/example/android/apis/app/JobWorkServiceActivity.java b/samples/ApiDemos/src/com/example/android/apis/app/JobWorkServiceActivity.java
index 5155466..d666ed6 100644
--- a/samples/ApiDemos/src/com/example/android/apis/app/JobWorkServiceActivity.java
+++ b/samples/ApiDemos/src/com/example/android/apis/app/JobWorkServiceActivity.java
@@ -42,7 +42,9 @@
mJobScheduler = (JobScheduler)getSystemService(JOB_SCHEDULER_SERVICE);
mJobInfo = new JobInfo.Builder(R.string.job_service_created,
- new ComponentName(this, JobWorkService.class)).setOverrideDeadline(0).build();
+ new ComponentName(this, JobWorkService.class))
+ .setRequiresBatteryNotLow(true)
+ .build();
setContentView(R.layout.job_work_service_activity);
diff --git a/samples/PictureInPicture/ComposePip/.gitignore b/samples/PictureInPicture/ComposePip/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/samples/PictureInPicture/ComposePip/app/.gitignore b/samples/PictureInPicture/ComposePip/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/build.gradle b/samples/PictureInPicture/ComposePip/app/build.gradle
new file mode 100644
index 0000000..4773587
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/build.gradle
@@ -0,0 +1,66 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ namespace 'com.example.samplepip'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.example.samplepip"
+ minSdk 24
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion '1.4.0'
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.10.1'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
+ implementation 'androidx.activity:activity-compose:1.7.2'
+ implementation "androidx.compose.ui:ui:$compose_ui_version"
+ implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
+ implementation 'androidx.compose.material3:material3:1.0.0'
+ implementation 'androidx.compose.material:material:1.3.0'
+ implementation "androidx.media3:media3-exoplayer:$media3_version"
+ implementation "androidx.media3:media3-exoplayer-dash:$media3_version"
+ implementation "androidx.media3:media3-ui:$media3_version"
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+ androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
+ debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
+ debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/proguard-rules.pro b/samples/PictureInPicture/ComposePip/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt b/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..3fa54fe
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/androidTest/java/com/example/samplepip/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.samplepip
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.samplepip", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml b/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..febc75a
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.SamplePip"
+ tools:targetApi="31">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:supportsPictureInPicture="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.SamplePip">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+
+ <meta-data
+ android:name="android.app.lib_name"
+ android:value=""/>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt
new file mode 100644
index 0000000..3946c16
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/MainActivity.kt
@@ -0,0 +1,68 @@
+package com.example.samplepip
+
+import android.app.PictureInPictureParams
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Rational
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.media3.exoplayer.ExoPlayer
+import com.example.samplepip.R.raw
+import com.example.samplepip.ui.theme.PiPComposeSampleTheme
+
+
+class MainActivity : ComponentActivity() {
+ private var player: ExoPlayer? = null
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ player = ExoPlayer.Builder(applicationContext).build()
+ setContent {
+ Column {
+ PiPComposeSampleTheme {
+ SampleVideoPlayer(
+ videoUri = Uri.parse("android.resource://$packageName/${raw.samplevideo}"),
+ modifier = Modifier.fillMaxWidth().pictureInPicture(onBoundsChange = ::onBoundsChange),
+ player = player!!
+
+ //TODO(b/276395464): only auto enter on unpaused state: add callback for playing and pausing
+ // as well as onUserLeaveHint
+ )
+ }
+
+ PipButton()
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @Composable
+ fun PipButton() {
+ Button(onClick = {
+ enterPictureInPictureMode()
+ }) {
+ Text(text = getString(R.string.start_pip_button))
+ }
+ }
+
+ private fun onBoundsChange(bounds: Rect) {
+ /**
+ * Whenever the players bounds change, we want to update our params
+ */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val builder = PictureInPictureParams.Builder()
+ .setSourceRectHint(bounds)
+ .setAspectRatio(Rational(bounds.width(), bounds.height()))
+ setPictureInPictureParams(builder.build())
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt
new file mode 100644
index 0000000..d403e76
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/SampleVideoPlayer.kt
@@ -0,0 +1,42 @@
+package com.example.samplepip
+
+import android.graphics.Rect
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toAndroidRectF
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.graphics.toRect
+import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.PlayerView
+
+@Composable
+fun SampleVideoPlayer(
+ videoUri: Uri,
+ modifier: Modifier,
+ player: ExoPlayer,
+) {
+ AndroidView(
+ factory = {
+ PlayerView(it).apply {
+ //TODO(b/276395464): set proper player functionality
+ setPlayer(player)
+ player.setMediaItem(MediaItem.fromUri(videoUri))
+ player.play()
+ }
+ },
+ modifier = modifier,
+ )
+}
+
+// callback to get player bounds, used to smoothly enter PIP
+fun Modifier.pictureInPicture(onBoundsChange: (Rect) -> Unit): Modifier {
+ return this then Modifier.onGloballyPositioned { layoutCoordinates ->
+ // Use onGloballyPositioned to get player bounds, which gets passed back to the main activity
+ val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
+ onBoundsChange(sourceRect)
+ }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt
new file mode 100644
index 0000000..4b133b1
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Color.kt
@@ -0,0 +1,8 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple200 = Color(0xFFBB86FC)
+val Purple500 = Color(0xFF6200EE)
+val Purple700 = Color(0xFF3700B3)
+val Teal200 = Color(0xFF03DAC5)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt
new file mode 100644
index 0000000..ef230e8
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt
new file mode 100644
index 0000000..aeff16d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Theme.kt
@@ -0,0 +1,60 @@
+package com.example.samplepip.ui.theme
+
+import android.app.Activity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple200,
+ secondary = Purple700,
+ tertiary = Teal200
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple500,
+ secondary = Purple700,
+ tertiary = Teal200
+
+ /* Other default colors to override
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+ */
+)
+
+@Composable
+fun PiPComposeSampleTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colorScheme =
+ if (!darkTheme) {
+ LightColorScheme
+ } else {
+ DarkColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view)?.isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt
new file mode 100644
index 0000000..5525539
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/java/com/example/samplepip/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.example.samplepip.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..305ef03
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path
+ android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0"/>
+ <item
+ android:color="#00000000"
+ android:offset="1.0"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeColor="#00000000"
+ android:strokeWidth="1"/>
+</vector>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..2be47c2
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z"/>
+ <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+ <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
+ android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
+</vector>
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6d5e5d0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background"/>
+ <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4 b/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4
new file mode 100644
index 0000000..7a5467e
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/raw/samplevideo.mp4
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..09837df
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0fb3ab3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<resources>
+ <string name="app_name">SamplePip</string>
+ <string name="start_pip_button">Enter Pip</string>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..7a2f21a
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Theme.SamplePip"
+ parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:statusBarColor">@color/purple_700</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..7a180f4
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Sample backup rules file; uncomment and customize as necessary.
+ See https://developer.android.com/guide/topics/data/autobackup
+ for details.
+ Note: This file is ignored for devices older that API 31
+ See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+ <!--
+ <include domain="sharedpref" path="."/>
+ <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..84910a7
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Sample data extraction rules file; uncomment and customize as necessary.
+ See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+ for details.
+-->
+<data-extraction-rules>
+ <cloud-backup>
+ <!-- TODO: Use <include> and <exclude> to control what is backed up.
+ <include .../>
+ <exclude .../>
+ -->
+ </cloud-backup>
+ <!--
+ <device-transfer>
+ <include .../>
+ <exclude .../>
+ </device-transfer>
+ -->
+</data-extraction-rules>
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt b/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt
new file mode 100644
index 0000000..51ce218
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/app/src/test/java/com/example/samplepip/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.samplepip
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/build.gradle b/samples/PictureInPicture/ComposePip/build.gradle
new file mode 100644
index 0000000..8a462e3
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/build.gradle
@@ -0,0 +1,11 @@
+buildscript {
+ ext {
+ compose_ui_version = '1.2.1'
+ media3_version = '1.1.0'
+ }
+}// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '7.3.0' apply false
+ id 'com.android.library' version '7.3.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
+}
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/gradle.properties b/samples/PictureInPicture/ComposePip/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..82abc92
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jul 11 03:45:22 UTC 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/samples/PictureInPicture/ComposePip/gradlew b/samples/PictureInPicture/ComposePip/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/samples/PictureInPicture/ComposePip/gradlew.bat b/samples/PictureInPicture/ComposePip/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/samples/PictureInPicture/ComposePip/settings.gradle b/samples/PictureInPicture/ComposePip/settings.gradle
new file mode 100644
index 0000000..1762602
--- /dev/null
+++ b/samples/PictureInPicture/ComposePip/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "SamplePip"
+include ':app'
diff --git a/samples/VirtualDeviceManager/Android.bp b/samples/VirtualDeviceManager/Android.bp
new file mode 100644
index 0000000..e7689b8
--- /dev/null
+++ b/samples/VirtualDeviceManager/Android.bp
@@ -0,0 +1,84 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "VdmHost",
+ manifest: "host/AndroidManifest.xml",
+ sdk_version: "system_current",
+ privileged: true,
+ srcs: [
+ "host/src/**/*.java"
+ ],
+ resource_dirs: [
+ "host/res",
+ ],
+ static_libs: [
+ "VdmCommonLib",
+ "android.companion.virtual.flags-aconfig-java",
+ "android.companion.virtualdevice.flags-aconfig-java",
+ "androidx.annotation_annotation",
+ "androidx.appcompat_appcompat",
+ "androidx.preference_preference",
+ "guava",
+ "hilt_android",
+ ],
+}
+
+android_app {
+ name: "VdmClient",
+ manifest: "client/AndroidManifest.xml",
+ sdk_version: "current",
+ srcs: [
+ "client/src/**/*.java"
+ ],
+ resource_dirs: [
+ "client/res",
+ ],
+ static_libs: [
+ "VdmCommonLib",
+ "androidx.annotation_annotation",
+ "androidx.appcompat_appcompat",
+ "androidx.recyclerview_recyclerview",
+ "androidx-constraintlayout_constraintlayout",
+ "guava",
+ "hilt_android",
+ ],
+}
+
+android_app {
+ name: "VdmDemos",
+ manifest: "demos/AndroidManifest.xml",
+ sdk_version: "current",
+ srcs: [
+ "demos/src/**/*.java"
+ ],
+ resource_dirs: [
+ "demos/res",
+ ],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.appcompat_appcompat",
+ ],
+}
+
+android_library {
+ name: "VdmCommonLib",
+ manifest: "common/AndroidManifest.xml",
+ sdk_version: "current",
+ srcs: [
+ "common/src/**/*.java",
+ "common/proto/*.proto",
+ ],
+ resource_dirs: [
+ "common/res",
+ ],
+ proto: {
+ type: "lite",
+ },
+ static_libs: [
+ "androidx.appcompat_appcompat",
+ "guava",
+ "hilt_android",
+ ],
+}
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/README.md b/samples/VirtualDeviceManager/README.md
new file mode 100644
index 0000000..7eb8fac
--- /dev/null
+++ b/samples/VirtualDeviceManager/README.md
@@ -0,0 +1,335 @@
+# VirtualDeviceManager Demo Apps
+
+#### Page contents
+
+[Overview](#overview) \
+[Prerequisites](#prerequisites) \
+[Build & Install](#build-and-install) \
+[Run](#run) \
+[Host Options](#host-options) \
+[Client Options](#client-options) \
+[Demos](#demos)
+
+## Overview
+
+The VDM Demo Apps allow for showcasing VDM features, rapid prototyping and
+testing of new features.
+
+The VDM Demo contains 3 apps:
+
+* **Host**: installed on the host device, creates and manages a virtual
+ device, which represents the client device and communicates with the
+ physical client device by sending audio and frames of the virtual displays,
+ receiving input and sensor data that is injected into the framework. It can
+ launch apps on the virtual device, which are streamed to the client.
+
+* **Client**: installed on the client device. It receives the audio and frames
+ from the host device, which it renders, and sends back input and sensor
+ data. For best experience with app streaming on multiple displays at the
+ same time, it's best to use a large screen device as a client, like a Pixel
+ Tablet.
+
+* **Demos**: installed on the host, meant to showcase specific VDM features.
+ The demos can be also run natively on the host to illustrate better the
+ difference in the behavior when they are streamed to a virtual device.
+
+## Prerequisites
+
+* An Android device running Android 13 or newer to act as a client device.
+
+* A *rooted* Android device running Android 14 or newer (e.g. a `userdebug` or
+ `eng` build) to act as a host device.
+
+* Both devices need to support
+ [Wi-Fi Aware](https://developer.android.com/develop/connectivity/wifi/wifi-aware)
+
+<!-- TODO(b/314429442): Make the host app work on older Android versions. -->
+
+Note: This example app uses an Android device as a client, but there's no such
+general requirement. The client device, its capabilities, the connectivity layer
+and the communication protocol are entirely up to the virtual device owner.
+
+## Build and Install
+
+### Using the script
+
+Simply connect your devices, navigate to the root of your Android checkout and
+run
+
+```
+./development/samples/VirtualDeviceManager/setup.sh
+```
+
+The interactive script will prompt you which apps to install to which of the
+available devices, build the APKs and install them.
+
+### Manually
+
+1. Source `build/envsetup.sh` and run `lunch` or set
+ `UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true` if there's no local build because
+ the APKs need to be built against a locally built SDK.
+
+1. Build the Host app.
+
+ ```shell
+ m -j VdmHost
+ ```
+
+1. Install the application as a system app on the host device.
+ <!-- TODO(b/314436863): Add a bash script for easy host app install. -->
+
+ ```shell
+ adb root && adb disable-verity && adb reboot # one time
+ adb root && adb remount
+ adb push $ANDROID_BUILD_TOP/development/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml /system/etc/permissions/com.example.android.vdmdemo.host.xml
+ adb shell mkdir /system/priv-app/VdmDemoHost
+ adb push $OUT/system/priv-app/VdmHost/VdmHost.apk /system/priv-app/VdmDemoHost/
+ adb reboot
+ ```
+
+ **Tip:** Subsequent installs without changes to permissions, etc. do not
+ need all the commands above - you can just run \
+ \
+ `adb install -r -d -g $OUT/system/priv-app/VdmHost/VdmHost.apk`
+
+1. Build and install the Demo app on the host device.
+
+ ```shell
+ m -j VdmDemos && adb install -r -d -g $OUT/system/app/VdmDemos/VdmDemos.apk
+ ```
+
+1. Build and install the Client app on the client device.
+
+ ```shell
+ m -j VdmClient && adb install -r -d -g $OUT/system/app/VdmClient/VdmClient.apk
+ ```
+
+## Run
+
+1. Start both the Client and the Host apps on each respective device.
+
+1. They should find each other and connect automatically. On the first launch
+ the Host app will ask to create a CDM association: allow it.
+
+ WARNING: If there are other devices in the vicinity with one of these apps
+ running, they might interfere.
+
+1. Check out the different [Host Options](#host-options) and
+ [Client Options](#client-options) that allow for changing the behavior of
+ the streamed apps and the virtual device in general.
+
+1. Check out the [Demo apps](#demos) that are specifically meant to showcase
+ the VDM features.
+
+<!-- LINT.IfChange(host_options) -->
+
+## Host Options
+
+NOTE: Any flag changes require device reboot or "Force stop" of the host app
+because the flag values are cached and evaluated only when the host app is
+starting. Alternatively, run: \
+\
+`adb shell am force-stop com.example.android.vdmdemo.host`
+
+### Launcher
+
+Once the connection with the client device is established, the Host app will
+show a launcher-like list of installed apps on the host device.
+
+- Clicking an app icon will create a new virtual display, launch the app there
+ and start streaming the display contents to the client. The client will show
+ the surface of that display and render its contents.
+
+- Long pressing on an app icon will open a dialog to select an existing
+ display to launch the app on instead of creating a new one.
+
+- The Host app has a **CREATE HOME DISPLAY** button, clicking it will create a
+ new virtual display, launch the secondary home activity there and start
+ streaming the display contents to the client. The display on the Client app
+ will have a home button, clicking it will navigate the streaming experience
+ back to the home activity. Run the commands below to enable this
+ functionality.
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+- The Host app has a **CREATE MIRROR DISPLAY** button, clicking it will create
+ a new virtual display, mirror the default host display there and start
+ streaming the display contents to the client. Run the commands below to
+ enable this functionality.
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.consistent_display_flags true
+ adb shell device_config put virtual_devices android.companion.virtual.flags.interactive_screen_mirror true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+### Settings
+
+#### General
+
+- **Device profile**: Enables device streaming CDM role as opposed to app
+ streaming role, with all differences in policies that this entails. \
+ *Changing this will recreate the virtual device.*
+
+- **Include streamed apps in recents**: Whether streamed apps should show up
+ in the host device's recent apps. Run the commands below to enable this
+ functionality. \
+ *This can be changed dynamically.*
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.dynamic_policy true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+- **Enable cross-device clipboard**: Whether to share the clipboard between
+ the host and the virtual device. If disabled, both devices will have their
+ own isolated clipboards. Run the commands below to enable this
+ functionality. \
+ *This can be changed dynamically.*
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.cross_device_clipboard true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+#### Client capabilities
+
+- **Enable client Sensors**: Enables sensor injection from the client device
+ into the host device. Any context that is associated with the virtual device
+ will access the virtual sensors by default. \
+ *Changing this will recreate the virtual device.*
+
+- **Enable client Audio**: Enables audio output on the client device. Any
+ context that is associated with the virtual device will play audio on the
+ client by default. \
+ *This can be changed dynamically.*
+
+#### Displays
+
+- **Display rotation**: Whether orientation change requests from streamed apps
+ should trigger orientation change of the relevant display. The client will
+ automatically rotate the relevant display upon such request. Disabling this
+ simulates a fixed orientation display that cannot physically rotate. Then
+ any streamed apps on that display will be letterboxed/pillarboxed if they
+ request orientation change. Run the commands below to enable this
+ functionality. \
+ *This can be changed dynamically but only applies to newly created
+ displays.*
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.consistent_display_flags true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+- **Always unlocked**: Whether the virtual displays should remain unlocked and
+ interactive when the host device is locked. Disabling this will result in a
+ simple lock screen shown on these displays when the host device is locked. \
+ *Changing this will recreate the virtual device.*
+
+- **Show pointer icon**: Whether pointer icon should be shown for virtual
+ input pointer devices. \
+ *This can be changed dynamically.*
+
+- **Custom home**: Whether to use a custom activity as home on home displays,
+ or use the device-default secondary home activity. Run the commands below to
+ enable this functionality. \
+ *Changing this will recreate the virtual device.*
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_home true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+#### Input method
+
+- **Display IME policy**: Choose the IME behavior on remote displays. Run the
+ commands below to enable this functionality. \
+ *This can be changed dynamically.*
+
+ ```shell
+ adb shell device_config put virtual_devices android.companion.virtual.flags.vdm_custom_ime true
+ adb shell am force-stop com.example.android.vdmdemo.host
+ ```
+
+#### Debug
+
+- **Record encoder output**: Enables recording the output of the encoder on
+ the host device to a local file on the device. This can be helpful with
+ debugging Encoding related issues. To download and play the file locally:
+
+ ```shell
+ adb pull /sdcard/Download/vdmdemo_encoder_output_<displayId>.h264
+ ffplay -f h264 vdmdemo_encoder_output_<displayId>.h264
+ ```
+
+<!-- LINT.ThenChange(README.md) -->
+<!-- LINT.IfChange(client_options) -->
+
+## Client Options
+
+### Streamed displays
+
+- Each display on the Client app has a "Back" and "Close" buttons. When a
+ display becomes empty, it's automatically removed.
+
+- Each display on the Client app has a "Rotate" button to switch between
+ portrait and landscape orientation. This simulates the physical rotation of
+ the display of the streamed activity. The "Resize" button can be used to
+ change the display dimensions.
+
+- Each display on the Client app has a "Fullscreen" button which will move the
+ contents of that display to an immersive fullscreen activity. The client's
+ back button/gestures are sent back to the streamed app. Use Volume Down on
+ the client device to exit fullscreen. Volume Up acts as a home key, if the
+ streamed display is a home display.
+
+### Input
+
+The input menu button enables **on-screen D-Pad and touchpad** for navigating
+the activity on the focused display. The focused display is indicated by the
+frame around its header whenever there are more than one displays. The display
+focus is based on user interaction.
+
+In addition, any input events generated from an **externally connected
+keyboard** are forwarded to the activity streamed on the focused display.
+
+**Externally connected mouse** events are also forwarded to the relevant
+display, if the mouse pointer is currently positioned on a streamed display.
+
+<!-- LINT.ThenChange(README.md) -->
+<!-- LINT.IfChange(demos) -->
+
+## Demos
+
+- **Sensors**: A simple activity balancing a beam on the screen based on the
+ accelerometer events, which allows for selecting which device's sensor to
+ use. By default, will use the sensors of the device it's shown on.
+
+- **Rotation**: A simple activity that is in landscape by default and can send
+ orientation change requests on demand. Showcases the display rotation on the
+ client, which will rotate the user-visible surface.
+
+- **Home**: A simple activity with utilities around launching HOME and
+ SECONDARY_HOME Intents.
+
+- **Secure Window**: A simple activity that declares the Window as secure.
+ This showcases the FLAG_SECURE streaming policies in VDM.
+
+- **Permissions**: A simple activity with buttons to request and revoke
+ runtime permissions. This can help test the permission streaming and
+ device-aware permission features.
+
+- **Latency**: Renders a simple counter view that renders a new frame with an
+ incremented counter every second. Can be useful for debugging latency,
+ encoder, decoder issues in the demo application.
+
+- **Vibration**: A simple activity making vibration requests via different
+ APIs and allows for selecting which device's vibrator to use. By default,
+ will use the vibrator of the device it's shown on. Note that currently there
+ is no vibration support on virtual devices, so vibration requests from
+ streamed activities are ignored.
+
+<!-- LINT.ThenChange(README.md) -->
diff --git a/samples/VirtualDeviceManager/client/AndroidManifest.xml b/samples/VirtualDeviceManager/client/AndroidManifest.xml
new file mode 100644
index 0000000..4fb9a33
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.vdmdemo.client"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="33"
+ android:targetSdkVersion="33" />
+
+ <uses-permission
+ android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS"
+ tools:ignore="HighSamplingRate" />
+
+ <application
+ android:name=".VdmClientApplication"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar">
+ <activity
+ android:name=".MainActivity"
+ android:screenOrientation="portrait"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".ImmersiveActivity"
+ android:configChanges="orientation|screenSize"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
+ </application>
+</manifest>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/back.xml b/samples/VirtualDeviceManager/client/res/drawable/back.xml
new file mode 100644
index 0000000..1028b9f
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/back.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M640,880L240,480L640,80L711,151L382,480L711,809L640,880Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/close.xml b/samples/VirtualDeviceManager/client/res/drawable/close.xml
new file mode 100644
index 0000000..d5d2297
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/close.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/dpad_center.xml b/samples/VirtualDeviceManager/client/res/drawable/dpad_center.xml
new file mode 100644
index 0000000..e7365ca
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/dpad_center.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/dpad_down.xml b/samples/VirtualDeviceManager/client/res/drawable/dpad_down.xml
new file mode 100644
index 0000000..68fd405
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/dpad_down.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/dpad_left.xml b/samples/VirtualDeviceManager/client/res/drawable/dpad_left.xml
new file mode 100644
index 0000000..d163db5
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/dpad_left.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M15.41,16.59L10.83,12l4.58,-4.59L14,6l-6,6 6,6 1.41,-1.41z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/dpad_right.xml b/samples/VirtualDeviceManager/client/res/drawable/dpad_right.xml
new file mode 100644
index 0000000..3c211b1
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/dpad_right.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/dpad_up.xml b/samples/VirtualDeviceManager/client/res/drawable/dpad_up.xml
new file mode 100644
index 0000000..ef2e982
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/dpad_up.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/focus_frame.xml b/samples/VirtualDeviceManager/client/res/drawable/focus_frame.xml
new file mode 100644
index 0000000..a995353
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/focus_frame.xml
@@ -0,0 +1,7 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ <stroke
+ android:width="5dp"
+ android:color="@android:color/holo_blue_light" />
+</shape>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/client/res/drawable/fullscreen.xml b/samples/VirtualDeviceManager/client/res/drawable/fullscreen.xml
new file mode 100644
index 0000000..1b7ad88
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/fullscreen.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/home.xml b/samples/VirtualDeviceManager/client/res/drawable/home.xml
new file mode 100644
index 0000000..ad899ed
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/home.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M160,840L160,360L480,120L800,360L800,840L560,840L560,560L400,560L400,840L160,840Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/input.xml b/samples/VirtualDeviceManager/client/res/drawable/input.xml
new file mode 100644
index 0000000..89e78b8
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/input.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,600L160,600L160,720Q160,720 160,720Q160,720 160,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,360L80,360L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM460,660L404,602L487,520L80,520L80,440L487,440L404,358L460,300L640,480L460,660Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/resize.xml b/samples/VirtualDeviceManager/client/res/drawable/resize.xml
new file mode 100644
index 0000000..79536e9
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/resize.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M760,360L760,200L600,200L600,120L840,120L840,360L760,360ZM120,840L120,600L200,600L200,760L360,760L360,840L120,840ZM120,520L120,440L200,440L200,520L120,520ZM120,360L120,280L200,280L200,360L120,360ZM120,200L120,120L200,120L200,200L120,200ZM280,200L280,120L360,120L360,200L280,200ZM440,840L440,760L520,760L520,840L440,840ZM440,200L440,120L520,120L520,200L440,200ZM600,840L600,760L680,760L680,840L600,840ZM760,840L760,760L840,760L840,840L760,840ZM760,680L760,600L840,600L840,680L760,680ZM760,520L760,440L840,440L840,520L760,520Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/drawable/resize_rect.xml b/samples/VirtualDeviceManager/client/res/drawable/resize_rect.xml
new file mode 100644
index 0000000..947efd2
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/resize_rect.xml
@@ -0,0 +1,8 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+ <corners android:radius="1dp" />
+ <stroke
+ android:width="3dp"
+ android:color="@android:color/darker_gray" />
+</shape>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/client/res/drawable/rotate.xml b/samples/VirtualDeviceManager/client/res/drawable/rotate.xml
new file mode 100644
index 0000000..5d94739
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/drawable/rotate.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M4,7.59l5,-5c0.78,-0.78 2.05,-0.78 2.83,0L20.24,11h-2.83L10.4,4L5.41,9H8v2H2V5h2V7.59zM20,19h2v-6h-6v2h2.59l-4.99,5l-7.01,-7H3.76l8.41,8.41c0.78,0.78 2.05,0.78 2.83,0l5,-5V19z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/client/res/layout/activity_immersive.xml b/samples/VirtualDeviceManager/client/res/layout/activity_immersive.xml
new file mode 100644
index 0000000..f9baeab
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/layout/activity_immersive.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextureView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/immersive_surface_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/client/res/layout/activity_main.xml b/samples/VirtualDeviceManager/client/res/layout/activity_main.xml
new file mode 100644
index 0000000..3180f6d
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/layout/activity_main.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/main_tool_bar"
+ android:layout_width="0dp"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="4dp"
+ android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/connectivity_fragment_container"
+ android:name="com.example.android.vdmdemo.common.ConnectivityFragment"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/main_tool_bar" />
+
+ <com.example.android.vdmdemo.client.ClientView
+ android:id="@+id/displays"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/guideline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/connectivity_fragment_container" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.8" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/dpad_fragment_container"
+ android:name="com.example.android.vdmdemo.client.DpadFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/nav_touchpad_fragment_container"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/guideline" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/nav_touchpad_fragment_container"
+ android:name="com.example.android.vdmdemo.client.NavTouchpadFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintStart_toEndOf="@+id/dpad_fragment_container"
+ app:layout_constraintTop_toBottomOf="@+id/guideline" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/samples/VirtualDeviceManager/client/res/layout/display_fragment.xml b/samples/VirtualDeviceManager/client/res/layout/display_fragment.xml
new file mode 100644
index 0000000..157b3a9
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/layout/display_fragment.xml
@@ -0,0 +1,98 @@
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:padding="5dp">
+
+ <LinearLayout
+ android:id="@+id/display_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintBottom_toTopOf="@id/remote_display_view"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed">
+
+ <TextView
+ android:id="@+id/display_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:padding="5dp"
+ android:text="@string/display_title" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/display_back"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_back"
+ android:src="@drawable/back" />
+
+ <ImageButton
+ android:id="@+id/display_home"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_home"
+ android:src="@drawable/home"
+ android:visibility="gone" />
+
+ <ImageButton
+ android:id="@+id/display_rotate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_rotate"
+ android:src="@drawable/rotate" />
+
+ <ImageButton
+ android:id="@+id/display_resize"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_resize"
+ android:src="@drawable/resize" />
+
+ <ImageButton
+ android:id="@+id/display_fullscreen"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_fullscreen"
+ android:src="@drawable/fullscreen" />
+
+ <ImageButton
+ android:id="@+id/display_close"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/display_close"
+ android:src="@drawable/close" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/strut"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@id/remote_display_view"
+ app:layout_constraintEnd_toEndOf="@id/display_header"
+ app:layout_constraintStart_toStartOf="@id/display_header"
+ app:layout_constraintTop_toBottomOf="@id/display_header" />
+
+ <TextureView
+ android:id="@+id/remote_display_view"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintDimensionRatio="9:16"
+ app:layout_constraintEnd_toEndOf="@id/display_header"
+ app:layout_constraintStart_toStartOf="@id/display_header"
+ app:layout_constraintTop_toBottomOf="@id/display_header" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/samples/VirtualDeviceManager/client/res/layout/fragment_dpad.xml b/samples/VirtualDeviceManager/client/res/layout/fragment_dpad.xml
new file mode 100644
index 0000000..d340f1b
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/layout/fragment_dpad.xml
@@ -0,0 +1,51 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="5dp"
+ tools:ignore="RtlHardcoded">
+
+ <ImageButton
+ android:id="@+id/dpad_center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:contentDescription="@string/dpad_center"
+ android:src="@drawable/dpad_center" />
+
+ <ImageButton
+ android:id="@+id/dpad_left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@id/dpad_center"
+ android:layout_toLeftOf="@id/dpad_center"
+ android:contentDescription="@string/dpad_left"
+ android:src="@drawable/dpad_left" />
+
+ <ImageButton
+ android:id="@+id/dpad_right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@id/dpad_center"
+ android:layout_toRightOf="@id/dpad_center"
+ android:contentDescription="@string/dpad_right"
+ android:src="@drawable/dpad_right" />
+
+ <ImageButton
+ android:id="@+id/dpad_up"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/dpad_center"
+ android:layout_alignLeft="@id/dpad_center"
+ android:contentDescription="@string/dpad_up"
+ android:src="@drawable/dpad_up" />
+
+ <ImageButton
+ android:id="@+id/dpad_down"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/dpad_center"
+ android:layout_alignLeft="@id/dpad_center"
+ android:contentDescription="@string/dpad_down"
+ android:src="@drawable/dpad_down" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/client/res/layout/fragment_nav_touchpad.xml b/samples/VirtualDeviceManager/client/res/layout/fragment_nav_touchpad.xml
new file mode 100644
index 0000000..021ed21
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/layout/fragment_nav_touchpad.xml
@@ -0,0 +1,8 @@
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/nav_touchpad"
+ style="@style/NavTouchpadStyle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:padding="5dp"
+ android:text="@string/nav_touchpad_move" />
diff --git a/samples/VirtualDeviceManager/client/res/menu/options.xml b/samples/VirtualDeviceManager/client/res/menu/options.xml
new file mode 100644
index 0000000..d1c02c4
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/menu/options.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- LINT.IfChange -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/input"
+ android:icon="@drawable/input"
+ android:title="@string/input"
+ app:showAsAction="always" />
+</menu>
+<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:client_options) -->
diff --git a/samples/VirtualDeviceManager/client/res/values/strings.xml b/samples/VirtualDeviceManager/client/res/values/strings.xml
new file mode 100644
index 0000000..4d782de
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/values/strings.xml
@@ -0,0 +1,19 @@
+<resources>
+ <string name="app_name" translatable="false">VDM Client</string>
+ <string name="display_title" translatable="false">Display %d. %s</string>
+ <string name="display_back" translatable="false">Back</string>
+ <string name="display_home" translatable="false">Home</string>
+ <string name="display_rotate" translatable="false">Rotate</string>
+ <string name="display_resize" translatable="false">Resize</string>
+ <string name="display_fullscreen" translatable="false">Fullscreen</string>
+ <string name="display_close" translatable="false">Close</string>
+
+ <string name="input" translatable="false">Input</string>
+
+ <string name="dpad_up" translatable="false">Up</string>
+ <string name="dpad_down" translatable="false">Down</string>
+ <string name="dpad_left" translatable="false">Left</string>
+ <string name="dpad_right" translatable="false">Right</string>
+ <string name="dpad_center" translatable="false">Center</string>
+ <string name="nav_touchpad_move" translatable="false">Navigation Touchpad</string>
+</resources>
diff --git a/samples/VirtualDeviceManager/client/res/values/styles.xml b/samples/VirtualDeviceManager/client/res/values/styles.xml
new file mode 100644
index 0000000..397a2b3
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/res/values/styles.xml
@@ -0,0 +1,15 @@
+<resources>
+
+ <style name="NavTouchpadStyle" parent="@style/Theme.AppCompat.Light.NoActionBar">
+ <item name="android:background">#DDDDDD</item>
+ </style>
+
+ <style name="Theme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+</resources>
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/AudioPlayer.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/AudioPlayer.java
new file mode 100644
index 0000000..31edd26
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/AudioPlayer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.util.Log;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.AudioFrame;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+final class AudioPlayer implements Consumer<RemoteEvent> {
+ private static final String TAG = AudioPlayer.class.getSimpleName();
+
+ private static final int SAMPLE_RATE = 44000;
+ private static final AudioFormat AUDIO_FORMAT =
+ new AudioFormat.Builder()
+ .setSampleRate(SAMPLE_RATE)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .build();
+ private static final AudioAttributes AUDIO_ATTRIBUTES =
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+ private static final int MIN_AUDIOTRACK_BUFFER_SIZE =
+ AudioTrack.getMinBufferSize(
+ SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
+ private static final int AUDIOTRACK_BUFFER_SIZE = 4 * MIN_AUDIOTRACK_BUFFER_SIZE;
+
+ private final Object mLock = new Object();
+ private AudioTrack mAudioTrack;
+
+ @Inject
+ AudioPlayer() {}
+
+ private void startPlayback() {
+ synchronized (mLock) {
+ if (mAudioTrack != null) {
+ Log.w(TAG, "Received startPlayback command without stopping the playback first");
+ stopPlayback();
+ }
+ mAudioTrack =
+ new AudioTrack.Builder()
+ .setAudioFormat(AUDIO_FORMAT)
+ .setAudioAttributes(AUDIO_ATTRIBUTES)
+ .setBufferSizeInBytes(AUDIOTRACK_BUFFER_SIZE)
+ .build();
+ mAudioTrack.play();
+ }
+ }
+
+ private void playAudioFrame(AudioFrame audioFrame) {
+ byte[] data = audioFrame.getData().toByteArray();
+ int bytesToWrite = data.length;
+ if (bytesToWrite == 0) {
+ return;
+ }
+ int bytesWritten = 0;
+ synchronized (mLock) {
+ if (mAudioTrack == null) {
+ Log.e(TAG, "Received audio frame, but audio track was not initialized yet");
+ return;
+ }
+
+ while (bytesToWrite > 0) {
+ int ret = mAudioTrack.write(data, bytesWritten, bytesToWrite);
+ if (ret <= 0) {
+ Log.e(TAG, "AudioTrack.write returned error code " + ret);
+ }
+ bytesToWrite -= ret;
+ bytesWritten += ret;
+ }
+ }
+ }
+
+ private void stopPlayback() {
+ synchronized (mLock) {
+ if (mAudioTrack == null) {
+ Log.w(TAG, "Received stopPlayback command for already stopped playback");
+ } else {
+ mAudioTrack.stop();
+ mAudioTrack.release();
+ mAudioTrack = null;
+ }
+ }
+ }
+
+ @Override
+ public void accept(RemoteEvent remoteEvent) {
+ if (remoteEvent.hasStartAudio()) {
+ startPlayback();
+ }
+ if (remoteEvent.hasAudioFrame()) {
+ playAudioFrame(remoteEvent.getAudioFrame());
+ }
+ if (remoteEvent.hasStopAudio()) {
+ stopPlayback();
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ClientView.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ClientView.java
new file mode 100644
index 0000000..0df2587
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ClientView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.StyleRes;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.function.Consumer;
+
+/** Recycler view that can resize a child dynamically. */
+public final class ClientView extends RecyclerView {
+
+ private static final int MIN_SIZE = 100;
+ private int mMaxSize = 0;
+
+ private boolean mIsResizing = false;
+ private Consumer<Rect> mResizeDoneCallback = null;
+ private Drawable mResizingRect = null;
+ private final Rect mResizingBounds = new Rect();
+ private float mResizeOffsetX = 0;
+ private float mResizeOffsetY = 0;
+
+ public ClientView(Context context) {
+ super(context);
+ init();
+ }
+
+ public ClientView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ClientView(Context context, AttributeSet attrs, @StyleRes int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ mResizingRect = getContext().getResources().getDrawable(R.drawable.resize_rect, null);
+ }
+
+ void startResizing(View viewToResize, MotionEvent origin, int maxSize,
+ Consumer<Rect> callback) {
+ mIsResizing = true;
+ mMaxSize = maxSize;
+ mResizeDoneCallback = callback;
+ viewToResize.getGlobalVisibleRect(mResizingBounds);
+ mResizingRect.setBounds(mResizingBounds);
+ getRootView().getOverlay().add(mResizingRect);
+ mResizeOffsetX = origin.getRawX() - mResizingBounds.right;
+ mResizeOffsetY = origin.getRawY() - mResizingBounds.top;
+ }
+
+ private void stopResizing() {
+ if (!mIsResizing) {
+ return;
+ }
+ mIsResizing = false;
+ mResizeOffsetX = mResizeOffsetY = 0;
+ getRootView().getOverlay().clear();
+ if (mResizeDoneCallback != null) {
+ mResizeDoneCallback.accept(mResizingBounds);
+ mResizeDoneCallback = null;
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ if (!mIsResizing) {
+ return super.dispatchTouchEvent(ev);
+ }
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_UP -> stopResizing();
+ case MotionEvent.ACTION_MOVE -> {
+ mResizingBounds.right = (int) (ev.getRawX() - mResizeOffsetX);
+ if (mResizingBounds.width() > mMaxSize) {
+ mResizingBounds.right = mResizingBounds.left + mMaxSize;
+ }
+ if (mResizingBounds.width() < MIN_SIZE) {
+ mResizingBounds.right = mResizingBounds.left + MIN_SIZE;
+ }
+ mResizingBounds.top = (int) (ev.getRawY() - mResizeOffsetY);
+ if (mResizingBounds.height() > mMaxSize) {
+ mResizingBounds.top = mResizingBounds.bottom - mMaxSize;
+ }
+ if (mResizingBounds.height() < MIN_SIZE) {
+ mResizingBounds.top = mResizingBounds.bottom - MIN_SIZE;
+ }
+ mResizingRect.setBounds(mResizingBounds);
+ }
+ }
+ return true;
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayAdapter.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayAdapter.java
new file mode 100644
index 0000000..bea221a
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayAdapter.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.example.android.vdmdemo.client.DisplayAdapter.DisplayHolder;
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteIo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+final class DisplayAdapter extends RecyclerView.Adapter<DisplayHolder> {
+ private static final String TAG = "VdmClient";
+
+ private static final AtomicInteger sNextDisplayIndex = new AtomicInteger(1);
+
+ // Simple list of all active displays.
+ private final List<RemoteDisplay> mDisplayRepository =
+ Collections.synchronizedList(new ArrayList<>());
+
+ private final RemoteIo mRemoteIo;
+ private final ClientView mRecyclerView;
+ private final InputManager mInputManager;
+ private ActivityResultLauncher<Intent> mFullscreenLauncher;
+
+ DisplayAdapter(ClientView recyclerView, RemoteIo remoteIo, InputManager inputManager) {
+ mRecyclerView = recyclerView;
+ mRemoteIo = remoteIo;
+ mInputManager = inputManager;
+ setHasStableIds(true);
+ }
+
+ void setFullscreenLauncher(ActivityResultLauncher<Intent> launcher) {
+ mFullscreenLauncher = launcher;
+ }
+
+ void onFullscreenActivityResult(ActivityResult result) {
+ Intent data = result.getData();
+ if (data == null) {
+ return;
+ }
+ int displayId =
+ data.getIntExtra(ImmersiveActivity.EXTRA_DISPLAY_ID, Display.INVALID_DISPLAY);
+ if (result.getResultCode() == ImmersiveActivity.RESULT_CLOSE) {
+ removeDisplay(displayId);
+ } else if (result.getResultCode() == ImmersiveActivity.RESULT_MINIMIZE) {
+ int requestedRotation =
+ data.getIntExtra(ImmersiveActivity.EXTRA_REQUESTED_ROTATION, 0);
+ rotateDisplay(displayId, requestedRotation);
+ }
+ }
+
+ void addDisplay(boolean homeSupported) {
+ Log.i(TAG, "Adding display " + sNextDisplayIndex);
+ mDisplayRepository.add(
+ new RemoteDisplay(sNextDisplayIndex.getAndIncrement(), homeSupported));
+ notifyItemInserted(mDisplayRepository.size() - 1);
+ }
+
+ void removeDisplay(int displayId) {
+ Log.i(TAG, "Removing display " + displayId);
+ for (int i = 0; i < mDisplayRepository.size(); ++i) {
+ if (displayId == mDisplayRepository.get(i).getDisplayId()) {
+ mDisplayRepository.remove(i);
+ notifyItemRemoved(i);
+ break;
+ }
+ }
+ }
+
+ void rotateDisplay(int displayId, int rotationDegrees) {
+ DisplayHolder holder = getDisplayHolder(displayId);
+ if (holder != null) {
+ holder.rotateDisplay(rotationDegrees, /* resize= */ false);
+ }
+ }
+
+ void processDisplayChange(RemoteEvent event) {
+ DisplayHolder holder = getDisplayHolder(event.getDisplayId());
+ if (holder != null) {
+ holder.setDisplayTitle(event.getDisplayChangeEvent().getTitle());
+ }
+ }
+
+ void clearDisplays() {
+ Log.i(TAG, "Clearing all displays");
+ int size = mDisplayRepository.size();
+ mDisplayRepository.clear();
+ notifyItemRangeRemoved(0, size);
+ }
+
+ void pauseAllDisplays() {
+ Log.i(TAG, "Pausing all displays");
+ forAllDisplays(DisplayHolder::pause);
+ }
+
+ void resumeAllDisplays() {
+ Log.i(TAG, "Resuming all displays");
+ forAllDisplays(DisplayHolder::resume);
+ }
+
+ private void forAllDisplays(Consumer<DisplayHolder> consumer) {
+ for (int i = 0; i < mDisplayRepository.size(); ++i) {
+ DisplayHolder holder =
+ (DisplayHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
+ if (holder != null) {
+ consumer.accept(holder);
+ }
+ }
+ }
+
+ private DisplayHolder getDisplayHolder(int displayId) {
+ for (int i = 0; i < mDisplayRepository.size(); ++i) {
+ if (displayId == mDisplayRepository.get(i).getDisplayId()) {
+ return (DisplayHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public DisplayHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ // Disable recycling so layout changes are not present in new displays.
+ mRecyclerView.getRecycledViewPool().setMaxRecycledViews(viewType, 0);
+ View view =
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.display_fragment, parent, false);
+ return new DisplayHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(DisplayHolder holder, int position) {
+ holder.onBind(position);
+ }
+
+ @Override
+ public void onViewRecycled(DisplayHolder holder) {
+ holder.close();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mDisplayRepository.get(position).getDisplayId();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDisplayRepository.size();
+ }
+
+ public class DisplayHolder extends ViewHolder {
+ private DisplayController mDisplayController = null;
+ private InputManager.FocusListener mFocusListener = null;
+ private Surface mSurface = null;
+ private TextureView mTextureView = null;
+ private TextView mDisplayTitle = null;
+ private View mRotateButton = null;
+ private int mDisplayId = 0;
+
+ DisplayHolder(View view) {
+ super(view);
+ }
+
+ void rotateDisplay(int rotationDegrees, boolean resize) {
+ if (mTextureView.getRotation() == rotationDegrees) {
+ return;
+ }
+ Log.i(TAG, "Rotating display " + mDisplayId + " to " + rotationDegrees);
+ mRotateButton.setEnabled(rotationDegrees == 0 || resize);
+
+ // Make sure the rotation is visible.
+ View strut = itemView.requireViewById(R.id.strut);
+ ViewGroup.LayoutParams layoutParams = strut.getLayoutParams();
+ layoutParams.width = Math.max(mTextureView.getWidth(), mTextureView.getHeight());
+ strut.setLayoutParams(layoutParams);
+ final int postRotationWidth = (resize || rotationDegrees % 180 != 0)
+ ? mTextureView.getHeight() : mTextureView.getWidth();
+
+ mTextureView
+ .animate()
+ .rotation(rotationDegrees)
+ .setDuration(420)
+ .withEndAction(
+ () -> {
+ if (resize) {
+ resizeDisplay(
+ new Rect(
+ 0,
+ 0,
+ mTextureView.getHeight(),
+ mTextureView.getWidth()));
+ }
+ layoutParams.width = postRotationWidth;
+ strut.setLayoutParams(layoutParams);
+ })
+ .start();
+ }
+
+ private void resizeDisplay(Rect newBounds) {
+ Log.i(TAG, "Resizing display " + mDisplayId + " to " + newBounds);
+ mDisplayController.setSurface(mSurface, newBounds.width(), newBounds.height());
+
+ ViewGroup.LayoutParams layoutParams = mTextureView.getLayoutParams();
+ layoutParams.width = newBounds.width();
+ layoutParams.height = newBounds.height();
+ mTextureView.setLayoutParams(layoutParams);
+ }
+
+ private void setDisplayTitle(String title) {
+ mDisplayTitle.setText(
+ itemView.getContext().getString(R.string.display_title, mDisplayId, title));
+ }
+
+ void close() {
+ if (mDisplayController != null) {
+ Log.i(TAG, "Closing DisplayHolder for display " + mDisplayId);
+ mInputManager.removeFocusListener(mFocusListener);
+ mInputManager.removeFocusableDisplay(mDisplayId);
+ mDisplayController.close();
+ mDisplayController = null;
+ }
+ }
+
+ void pause() {
+ mDisplayController.pause();
+ }
+
+ void resume() {
+ mDisplayController.setSurface(
+ mSurface, mTextureView.getWidth(), mTextureView.getHeight());
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ void onBind(int position) {
+ RemoteDisplay remoteDisplay = mDisplayRepository.get(position);
+ mDisplayId = remoteDisplay.getDisplayId();
+ Log.v(TAG, "Binding DisplayHolder for display " + mDisplayId + " to position "
+ + position);
+
+ mDisplayTitle = itemView.requireViewById(R.id.display_title);
+ mTextureView = itemView.requireViewById(R.id.remote_display_view);
+ final View displayHeader = itemView.requireViewById(R.id.display_header);
+
+ mFocusListener =
+ focusedDisplayId -> {
+ if (focusedDisplayId == mDisplayId && mDisplayRepository.size() > 1) {
+ displayHeader.setBackgroundResource(R.drawable.focus_frame);
+ } else {
+ displayHeader.setBackground(null);
+ }
+ };
+ mInputManager.addFocusListener(mFocusListener);
+
+ mDisplayController = new DisplayController(mDisplayId, mRemoteIo);
+ Log.v(TAG, "Creating new DisplayController for display " + mDisplayId);
+
+ setDisplayTitle("");
+
+ View closeButton = itemView.requireViewById(R.id.display_close);
+ closeButton.setOnClickListener(
+ v -> ((DisplayAdapter) Objects.requireNonNull(getBindingAdapter()))
+ .removeDisplay(mDisplayId));
+
+ View backButton = itemView.requireViewById(R.id.display_back);
+ backButton.setOnClickListener(v -> mInputManager.sendBack(mDisplayId));
+
+ View homeButton = itemView.requireViewById(R.id.display_home);
+ if (remoteDisplay.isHomeSupported()) {
+ homeButton.setVisibility(View.VISIBLE);
+ homeButton.setOnClickListener(v -> mInputManager.sendHome(mDisplayId));
+ } else {
+ homeButton.setVisibility(View.GONE);
+ }
+
+ mRotateButton = itemView.requireViewById(R.id.display_rotate);
+ mRotateButton.setOnClickListener(v -> {
+ // This rotation is simply resizing the display with width with height swapped.
+ mDisplayController.setSurface(
+ mSurface,
+ /* width= */ mTextureView.getHeight(),
+ /* height= */ mTextureView.getWidth());
+ rotateDisplay(mTextureView.getWidth() > mTextureView.getHeight() ? 90 : -90, true);
+ });
+
+ View resizeButton = itemView.requireViewById(R.id.display_resize);
+ resizeButton.setOnTouchListener((v, event) -> {
+ if (event.getAction() != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ int maxSize = itemView.getHeight() - displayHeader.getHeight()
+ - itemView.getPaddingTop() - itemView.getPaddingBottom();
+ mRecyclerView.startResizing(
+ mTextureView, event, maxSize, DisplayHolder.this::resizeDisplay);
+ return true;
+ });
+
+ View fullscreenButton = itemView.requireViewById(R.id.display_fullscreen);
+ fullscreenButton.setOnClickListener(v -> {
+ Intent intent = new Intent(v.getContext(), ImmersiveActivity.class);
+ intent.putExtra(ImmersiveActivity.EXTRA_DISPLAY_ID, mDisplayId);
+ mFullscreenLauncher.launch(intent);
+ });
+
+ mTextureView.setOnTouchListener(
+ (v, event) -> {
+ if (event.getDevice().supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) {
+ mTextureView.getParent().requestDisallowInterceptTouchEvent(true);
+ mInputManager.sendInputEvent(
+ InputDeviceType.DEVICE_TYPE_TOUCHSCREEN, event, mDisplayId);
+ }
+ return true;
+ });
+ mTextureView.setSurfaceTextureListener(
+ new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureUpdated(@NonNull SurfaceTexture texture) {}
+
+ @Override
+ public void onSurfaceTextureAvailable(
+ @NonNull SurfaceTexture texture, int width, int height) {
+ Log.v(TAG, "Setting surface for display " + mDisplayId);
+ mInputManager.addFocusableDisplay(mDisplayId);
+ mSurface = new Surface(texture);
+ mDisplayController.setSurface(mSurface, width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture texture) {
+ Log.v(TAG, "onSurfaceTextureDestroyed for display " + mDisplayId);
+ if (mDisplayController != null) {
+ mDisplayController.pause();
+ }
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ @NonNull SurfaceTexture texture, int width, int height) {
+ Log.v(TAG, "onSurfaceTextureSizeChanged for display " + mDisplayId);
+ mTextureView.setRotation(0);
+ mRotateButton.setEnabled(true);
+ }
+ });
+ mTextureView.setOnGenericMotionListener(
+ (v, event) -> {
+ if (event.getDevice() == null
+ || !event.getDevice().supportsSource(InputDevice.SOURCE_MOUSE)) {
+ return false;
+ }
+ mInputManager.sendInputEvent(
+ InputDeviceType.DEVICE_TYPE_MOUSE, event, mDisplayId);
+ return true;
+ });
+ }
+ }
+
+ private static class RemoteDisplay {
+ // Local ID, not corresponding to the displayId of the relevant Display on the host device.
+ private final int mDisplayId;
+ private final boolean mHomeSupported;
+
+ RemoteDisplay(int displayId, boolean homeSupported) {
+ mDisplayId = displayId;
+ mHomeSupported = homeSupported;
+ }
+
+ int getDisplayId() {
+ return mDisplayId;
+ }
+
+ boolean isHomeSupported() {
+ return mHomeSupported;
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayController.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayController.java
new file mode 100644
index 0000000..cf4011f
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DisplayController.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.view.Surface;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.DisplayCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.StopStreaming;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.example.android.vdmdemo.common.VideoManager;
+
+final class DisplayController {
+ private static final int DPI = 300;
+
+ private final int mDisplayId;
+ private final RemoteIo mRemoteIo;
+
+ private VideoManager mVideoManager = null;
+
+ private int mDpi = DPI;
+ private RemoteEvent mDisplayCapabilities;
+
+ DisplayController(int displayId, RemoteIo remoteIo) {
+ mDisplayId = displayId;
+ mRemoteIo = remoteIo;
+ }
+
+ void setDpi(int dpi) {
+ mDpi = dpi;
+ }
+
+ void close() {
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(mDisplayId)
+ .setStopStreaming(StopStreaming.newBuilder())
+ .build());
+
+ if (mVideoManager != null) {
+ mVideoManager.stop();
+ }
+ }
+
+ void pause() {
+ if (mVideoManager == null) {
+ return;
+ }
+ mVideoManager.stop();
+ mVideoManager = null;
+
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(mDisplayId)
+ .setStopStreaming(StopStreaming.newBuilder().setPause(true))
+ .build());
+ }
+
+ void sendDisplayCapabilities() {
+ mRemoteIo.sendMessage(mDisplayCapabilities);
+ }
+
+ void setSurface(Surface surface, int width, int height) {
+ if (mVideoManager != null) {
+ mVideoManager.stop();
+ }
+ mVideoManager = VideoManager.createDecoder(mDisplayId, mRemoteIo);
+ mVideoManager.startDecoding(surface, width, height);
+ mDisplayCapabilities =
+ RemoteEvent.newBuilder()
+ .setDisplayId(mDisplayId)
+ .setDisplayCapabilities(
+ DisplayCapabilities.newBuilder()
+ .setViewportWidth(width)
+ .setViewportHeight(height)
+ .setDensityDpi(mDpi))
+ .build();
+ sendDisplayCapabilities();
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DpadFragment.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DpadFragment.java
new file mode 100644
index 0000000..867bd15
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/DpadFragment.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import androidx.fragment.app.Fragment;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/** Fragment to show UI for a Dpad. */
+@AndroidEntryPoint(Fragment.class)
+public final class DpadFragment extends Hilt_DpadFragment {
+ private static final String TAG = "DpadFragment";
+
+ private static final int[] BUTTONS = {
+ R.id.dpad_center, R.id.dpad_down, R.id.dpad_left, R.id.dpad_up, R.id.dpad_right
+ };
+
+ @Inject InputManager mInputManager;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, ViewGroup parent, Bundle savedInstanceState) {
+ View view = layoutInflater.inflate(R.layout.fragment_dpad, parent, false);
+
+ // Set the callback for all the buttons
+ // Note: the onClick XML attribute cannot be used with fragments, only activities.
+ for (int buttonId : BUTTONS) {
+ ImageButton button = view.requireViewById(buttonId);
+ button.setOnTouchListener(this::onDpadButtonClick);
+ }
+
+ return view;
+ }
+
+ private boolean onDpadButtonClick(View v, MotionEvent e) {
+ int action;
+ if (e.getAction() == MotionEvent.ACTION_DOWN) {
+ action = KeyEvent.ACTION_DOWN;
+ } else if (e.getAction() == MotionEvent.ACTION_UP) {
+ action = KeyEvent.ACTION_UP;
+ } else {
+ return false;
+ }
+
+ int keyCode;
+ int id = v.getId();
+ if (id == R.id.dpad_center) {
+ keyCode = KeyEvent.KEYCODE_DPAD_CENTER;
+ } else if (id == R.id.dpad_down) {
+ keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
+ } else if (id == R.id.dpad_left) {
+ keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
+ } else if (id == R.id.dpad_up) {
+ keyCode = KeyEvent.KEYCODE_DPAD_UP;
+ } else if (id == R.id.dpad_right) {
+ keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ Log.w(TAG, "onDpadButtonClick: Method called from a non Dpad button");
+ return false;
+ }
+ mInputManager.sendInputEventToFocusedDisplay(
+ InputDeviceType.DEVICE_TYPE_DPAD,
+ new KeyEvent(
+ /* downTime= */ System.currentTimeMillis(),
+ /* eventTime= */ System.currentTimeMillis(),
+ action,
+ keyCode,
+ /* repeat= */ 0));
+ return true;
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ImmersiveActivity.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ImmersiveActivity.java
new file mode 100644
index 0000000..af13791
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/ImmersiveActivity.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.SurfaceTexture;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.TextureView;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+
+import com.example.android.vdmdemo.common.ConnectionManager;
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteIo;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * VDM Client activity, showing apps running on a host device and sending input back to the host.
+ */
+@AndroidEntryPoint(AppCompatActivity.class)
+public class ImmersiveActivity extends Hilt_ImmersiveActivity {
+
+ private static final String TAG = "VdmClientImmersiveActivity";
+
+ static final String EXTRA_DISPLAY_ID = "displayId";
+ static final String EXTRA_REQUESTED_ROTATION = "requestedRotation";
+
+ static final int RESULT_MINIMIZE = 1;
+ static final int RESULT_CLOSE = 2;
+
+ // Approximately, see
+ // https://developer.android.com/reference/android/util/DisplayMetrics#density
+ private static final float DIP_TO_DPI = 160f;
+
+ @Inject ConnectionManager mConnectionManager;
+ @Inject RemoteIo mRemoteIo;
+ @Inject VirtualSensorController mSensorController;
+ @Inject AudioPlayer mAudioPlayer;
+ @Inject InputManager mInputManager;
+
+ private int mDisplayId = Display.INVALID_DISPLAY;
+ private DisplayController mDisplayController;
+ private Surface mSurface;
+
+ private int mPortraitWidth;
+ private int mPortraitHeight;
+ private int mRequestedRotation = 0;
+
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+
+ private final ConnectionManager.ConnectionCallback mConnectionCallback =
+ new ConnectionManager.ConnectionCallback() {
+
+ @Override
+ public void onDisconnected() {
+ finish(/* minimize= */ false);
+ }
+ };
+
+ @Override
+ @SuppressLint("ClickableViewAccessibility")
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_immersive);
+
+ WindowInsetsControllerCompat windowInsetsController =
+ WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
+ windowInsetsController.setSystemBarsBehavior(
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
+
+ mDisplayId = getIntent().getIntExtra(EXTRA_DISPLAY_ID, Display.INVALID_DISPLAY);
+
+ OnBackPressedCallback callback =
+ new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ mInputManager.sendBack(mDisplayId);
+ }
+ };
+ getOnBackPressedDispatcher().addCallback(this, callback);
+
+ mDisplayController = new DisplayController(mDisplayId, mRemoteIo);
+ mDisplayController.setDpi((int) (getResources().getDisplayMetrics().density * DIP_TO_DPI));
+
+ TextureView textureView = requireViewById(R.id.immersive_surface_view);
+ textureView.setOnTouchListener(
+ (v, event) -> {
+ if (event.getDevice().supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) {
+ textureView.getParent().requestDisallowInterceptTouchEvent(true);
+ mInputManager.sendInputEvent(
+ InputDeviceType.DEVICE_TYPE_TOUCHSCREEN, event, mDisplayId);
+ }
+ return true;
+ });
+ textureView.setSurfaceTextureListener(
+ new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureUpdated(@NonNull SurfaceTexture texture) {}
+
+ @Override
+ public void onSurfaceTextureAvailable(
+ @NonNull SurfaceTexture texture, int width, int height) {
+ Log.v(TAG, "Setting surface for immersive display " + mDisplayId);
+ mSurface = new Surface(texture);
+ mPortraitWidth = Math.min(width, height);
+ mPortraitHeight = Math.max(width, height);
+ mDisplayController.setSurface(mSurface, width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture texture) {
+ Log.v(TAG, "onSurfaceTextureDestroyed for immersive display " + mDisplayId);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ @NonNull SurfaceTexture texture, int width, int height) {}
+ });
+ textureView.setOnGenericMotionListener(
+ (v, event) -> {
+ if (event.getDevice() == null
+ || !event.getDevice().supportsSource(InputDevice.SOURCE_MOUSE)) {
+ return false;
+ }
+ mInputManager.sendInputEvent(
+ InputDeviceType.DEVICE_TYPE_MOUSE, event, mDisplayId);
+ return true;
+ });
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mConnectionManager.addConnectionCallback(mConnectionCallback);
+ mRemoteIo.addMessageConsumer(mAudioPlayer);
+ mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mConnectionManager.removeConnectionCallback(mConnectionCallback);
+ mRemoteIo.removeMessageConsumer(mAudioPlayer);
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mSensorController.close();
+ }
+
+ private void processRemoteEvent(RemoteEvent event) {
+ if (event.hasStopStreaming()) {
+ finish(/* minimize= */ false);
+ } else if (event.hasDisplayRotation()) {
+ mRequestedRotation = event.getDisplayRotation().getRotationDegrees();
+ }
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_VOLUME_UP -> mInputManager.sendHome(mDisplayId);
+ case KeyEvent.KEYCODE_VOLUME_DOWN -> finish(/* minimize= */ true);
+ case KeyEvent.KEYCODE_BACK -> {
+ return super.dispatchKeyEvent(event);
+ }
+ default -> mInputManager.sendInputEvent(
+ InputDeviceType.DEVICE_TYPE_KEYBOARD, event, mDisplayId);
+ }
+ return true;
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration config) {
+ super.onConfigurationChanged(config);
+ if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ Log.d(TAG, "Switching landscape");
+ mDisplayController.setSurface(
+ mSurface, /* width= */ mPortraitHeight, /* height= */ mPortraitWidth);
+ } else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) {
+ Log.d(TAG, "Switching to portrait");
+ mDisplayController.setSurface(mSurface, mPortraitWidth, mPortraitHeight);
+ }
+ }
+
+ private void finish(boolean minimize) {
+ if (minimize) {
+ mDisplayController.close();
+ } else {
+ mDisplayController.pause();
+ }
+ Intent result = new Intent();
+ result.putExtra(EXTRA_DISPLAY_ID, mDisplayId);
+ result.putExtra(EXTRA_REQUESTED_ROTATION, mRequestedRotation);
+ setResult(minimize ? RESULT_MINIMIZE : RESULT_CLOSE, result);
+ finish();
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/InputManager.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/InputManager.java
new file mode 100644
index 0000000..285cf52
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/InputManager.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.util.Log;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import androidx.annotation.GuardedBy;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteHomeEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteInputEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteKeyEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteMotionEvent;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.google.common.collect.Iterables;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Maintains focused display and handles injection of targeted and untargeted input events. */
+@Singleton
+final class InputManager {
+ private static final String TAG = "InputManager";
+
+ private final RemoteIo mRemoteIo;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private int mFocusedDisplayId = Display.INVALID_DISPLAY;
+
+ interface FocusListener {
+ void onFocusChange(int focusedDisplayId);
+ }
+
+ @GuardedBy("mLock")
+ private final List<FocusListener> mFocusListeners = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private final Set<Integer> mFocusableDisplays = new HashSet<>();
+
+ @Inject
+ InputManager(RemoteIo remoteIo) {
+ mRemoteIo = remoteIo;
+ }
+
+ void addFocusListener(FocusListener focusListener) {
+ synchronized (mLock) {
+ mFocusListeners.add(focusListener);
+ }
+ }
+
+ void removeFocusListener(FocusListener focusListener) {
+ synchronized (mLock) {
+ mFocusListeners.remove(focusListener);
+ }
+ }
+
+ void addFocusableDisplay(int displayId) {
+ synchronized (mLock) {
+ if (mFocusableDisplays.add(displayId)) {
+ setFocusedDisplayId(displayId);
+ }
+ }
+ }
+
+ void removeFocusableDisplay(int displayId) {
+ synchronized (mLock) {
+ mFocusableDisplays.remove(displayId);
+ if (displayId == mFocusedDisplayId) {
+ setFocusedDisplayId(updateFocusedDisplayId());
+ }
+ }
+ }
+
+ /** Injects {@link InputEvent} for the given {@link InputDeviceType} into the given display. */
+ void sendInputEvent(InputDeviceType deviceType, InputEvent inputEvent, int displayId) {
+ if (inputEvent instanceof MotionEvent) {
+ MotionEvent event = (MotionEvent) inputEvent;
+ switch (deviceType) {
+ case DEVICE_TYPE_NAVIGATION_TOUCHPAD:
+ case DEVICE_TYPE_TOUCHSCREEN:
+ sendTouchEvent(deviceType, event, displayId);
+ break;
+ case DEVICE_TYPE_MOUSE:
+ sendMouseEvent(event, displayId);
+ break;
+ default:
+ Log.e(TAG, "sendInputEvent got invalid device type " + deviceType.getNumber());
+ }
+ } else {
+ KeyEvent event = (KeyEvent) inputEvent;
+ sendKeyEvent(deviceType, event, displayId);
+ }
+ }
+
+ /**
+ * Injects {@link InputEvent} for the given {@link InputDeviceType} into the focused display.
+ *
+ * @return whether the event was sent.
+ */
+ public boolean sendInputEventToFocusedDisplay(
+ InputDeviceType deviceType, InputEvent inputEvent) {
+ int targetDisplayId;
+ synchronized (mLock) {
+ if (mFocusedDisplayId == Display.INVALID_DISPLAY) {
+ return false;
+ }
+ targetDisplayId = mFocusedDisplayId;
+ }
+ sendInputEvent(deviceType, inputEvent, targetDisplayId);
+ return true;
+ }
+
+ void sendBack(int displayId) {
+ setFocusedDisplayId(displayId);
+ for (int action : new int[] {KeyEvent.ACTION_DOWN, KeyEvent.ACTION_UP}) {
+ sendInputEvent(
+ RemoteInputEvent.newBuilder()
+ .setDeviceType(InputDeviceType.DEVICE_TYPE_DPAD)
+ .setKeyEvent(
+ RemoteKeyEvent.newBuilder()
+ .setAction(action)
+ .setKeyCode(KeyEvent.KEYCODE_BACK))
+ .build(),
+ displayId);
+ }
+ }
+
+ void sendHome(int displayId) {
+ setFocusedDisplayId(displayId);
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(displayId)
+ .setHomeEvent(RemoteHomeEvent.newBuilder())
+ .build());
+ }
+
+ private void sendTouchEvent(InputDeviceType deviceType, MotionEvent event, int displayId) {
+ setFocusedDisplayId(displayId);
+ for (int pointerIndex = 0; pointerIndex < event.getPointerCount(); pointerIndex++) {
+ sendInputEvent(
+ RemoteInputEvent.newBuilder()
+ .setDeviceType(deviceType)
+ .setTimestampMs(event.getEventTime())
+ .setTouchEvent(
+ RemoteMotionEvent.newBuilder()
+ .setPointerId(event.getPointerId(pointerIndex))
+ .setAction(event.getActionMasked())
+ .setX(event.getX(pointerIndex))
+ .setY(event.getY(pointerIndex))
+ .setPressure(event.getPressure(pointerIndex)))
+ .build(),
+ displayId);
+ }
+ }
+
+ private void sendKeyEvent(InputDeviceType deviceType, KeyEvent event, int displayId) {
+ sendInputEvent(
+ RemoteInputEvent.newBuilder()
+ .setDeviceType(deviceType)
+ .setTimestampMs(event.getEventTime())
+ .setKeyEvent(
+ RemoteKeyEvent.newBuilder()
+ .setAction(event.getAction())
+ .setKeyCode(event.getKeyCode())
+ .build())
+ .build(),
+ displayId);
+ }
+
+ private void sendMouseEvent(MotionEvent event, int displayId) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_BUTTON_PRESS:
+ case MotionEvent.ACTION_BUTTON_RELEASE:
+ RemoteInputEvent buttonEvent =
+ RemoteInputEvent.newBuilder()
+ .setTimestampMs(System.currentTimeMillis())
+ .setDeviceType(InputDeviceType.DEVICE_TYPE_MOUSE)
+ .setMouseButtonEvent(
+ RemoteKeyEvent.newBuilder()
+ .setAction(event.getAction())
+ .setKeyCode(event.getActionButton())
+ .build())
+ .build();
+ sendInputEvent(buttonEvent, displayId);
+ break;
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_EXIT:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ setFocusedDisplayId(displayId);
+ RemoteInputEvent relativeEvent =
+ RemoteInputEvent.newBuilder()
+ .setTimestampMs(System.currentTimeMillis())
+ .setDeviceType(InputDeviceType.DEVICE_TYPE_MOUSE)
+ .setMouseRelativeEvent(
+ RemoteMotionEvent.newBuilder()
+ .setX(event.getX())
+ .setY(event.getY())
+ .build())
+ .build();
+ sendInputEvent(relativeEvent, displayId);
+ break;
+ case MotionEvent.ACTION_SCROLL:
+ float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+ float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+ RemoteInputEvent scrollEvent =
+ RemoteInputEvent.newBuilder()
+ .setTimestampMs(System.currentTimeMillis())
+ .setDeviceType(InputDeviceType.DEVICE_TYPE_MOUSE)
+ .setMouseScrollEvent(
+ RemoteMotionEvent.newBuilder()
+ .setX(clampMouseScroll(scrollX))
+ .setY(clampMouseScroll(scrollY))
+ .build())
+ .build();
+ sendInputEvent(scrollEvent, displayId);
+ break;
+ }
+ }
+
+ private void sendInputEvent(RemoteInputEvent inputEvent, int displayId) {
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder().setDisplayId(displayId).setInputEvent(inputEvent).build());
+ }
+
+ private static float clampMouseScroll(float val) {
+ return Math.max(Math.min(val, 1f), -1f);
+ }
+
+ private int updateFocusedDisplayId() {
+ synchronized (mLock) {
+ if (mFocusableDisplays.contains(mFocusedDisplayId)) {
+ return mFocusedDisplayId;
+ }
+ return Iterables.getFirst(mFocusableDisplays, Display.INVALID_DISPLAY);
+ }
+ }
+
+ private void setFocusedDisplayId(int displayId) {
+ List<FocusListener> listenersToNotify = Collections.emptyList();
+ synchronized (mLock) {
+ if (displayId != mFocusedDisplayId) {
+ mFocusedDisplayId = displayId;
+ listenersToNotify = new ArrayList<>(mFocusListeners);
+ }
+ }
+ for (FocusListener focusListener : listenersToNotify) {
+ focusListener.onFocusChange(displayId);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/MainActivity.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/MainActivity.java
new file mode 100644
index 0000000..22482fa
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/MainActivity.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.example.android.vdmdemo.common.ConnectionManager;
+import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteIo;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * VDM Client activity, showing apps running on a host device and sending input back to the host.
+ */
+@AndroidEntryPoint(AppCompatActivity.class)
+public class MainActivity extends Hilt_MainActivity {
+
+ @Inject RemoteIo mRemoteIo;
+ @Inject ConnectionManager mConnectionManager;
+ @Inject InputManager mInputManager;
+ @Inject VirtualSensorController mSensorController;
+ @Inject AudioPlayer mAudioPlayer;
+
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+ private DisplayAdapter mDisplayAdapter;
+
+ private final ConnectionManager.ConnectionCallback mConnectionCallback =
+ new ConnectionManager.ConnectionCallback() {
+ @Override
+ public void onConnected(String remoteDeviceName) {
+ mRemoteIo.sendMessage(RemoteEvent.newBuilder()
+ .setDeviceCapabilities(DeviceCapabilities.newBuilder()
+ .setDeviceName(Build.MODEL)
+ .addAllSensorCapabilities(
+ mSensorController.getSensorCapabilities()))
+ .build());
+ }
+
+ @Override
+ public void onDisconnected() {
+ if (mDisplayAdapter != null) {
+ runOnUiThread(mDisplayAdapter::clearDisplays);
+ }
+ mConnectionManager.startClientSession();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = requireViewById(R.id.main_tool_bar);
+ setSupportActionBar(toolbar);
+
+ ClientView displaysView = requireViewById(R.id.displays);
+ displaysView.setLayoutManager(
+ new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
+ displaysView.setItemAnimator(null);
+ mDisplayAdapter = new DisplayAdapter(displaysView, mRemoteIo, mInputManager);
+ displaysView.setAdapter(mDisplayAdapter);
+
+ ActivityResultLauncher<Intent> fullscreenLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ mDisplayAdapter::onFullscreenActivityResult);
+ mDisplayAdapter.setFullscreenLauncher(fullscreenLauncher);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mConnectionManager.addConnectionCallback(mConnectionCallback);
+ mConnectionManager.startClientSession();
+ mRemoteIo.addMessageConsumer(mAudioPlayer);
+ mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mDisplayAdapter.resumeAllDisplays();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mDisplayAdapter.pauseAllDisplays();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mConnectionManager.removeConnectionCallback(mConnectionCallback);
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ mRemoteIo.removeMessageConsumer(mAudioPlayer);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mDisplayAdapter.clearDisplays();
+ mConnectionManager.disconnect();
+ mSensorController.close();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mInputManager.sendInputEventToFocusedDisplay(
+ InputDeviceType.DEVICE_TYPE_KEYBOARD, event)
+ || super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.input -> toggleInputVisibility();
+ default -> {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ return true;
+ }
+
+ private void processRemoteEvent(RemoteEvent event) {
+ if (event.hasStartStreaming()) {
+ runOnUiThread(
+ () -> mDisplayAdapter.addDisplay(event.getStartStreaming().getHomeEnabled()));
+ } else if (event.hasStopStreaming()) {
+ runOnUiThread(() -> mDisplayAdapter.removeDisplay(event.getDisplayId()));
+ } else if (event.hasDisplayRotation()) {
+ runOnUiThread(() -> mDisplayAdapter.rotateDisplay(
+ event.getDisplayId(), event.getDisplayRotation().getRotationDegrees()));
+ } else if (event.hasDisplayChangeEvent()) {
+ runOnUiThread(() -> mDisplayAdapter.processDisplayChange(event));
+ }
+ }
+
+ private void toggleInputVisibility() {
+ View dpad = requireViewById(R.id.dpad_fragment_container);
+ int visibility = dpad.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE;
+ dpad.setVisibility(visibility);
+ requireViewById(R.id.nav_touchpad_fragment_container).setVisibility(visibility);
+ }
+}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/NavTouchpadFragment.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/NavTouchpadFragment.java
new file mode 100644
index 0000000..b78b967
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/NavTouchpadFragment.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/** Fragment to show UI for a navigation touchpad. */
+@AndroidEntryPoint(Fragment.class)
+public final class NavTouchpadFragment extends Hilt_NavTouchpadFragment {
+
+ @Inject InputManager mInputManager;
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_nav_touchpad, container, false);
+
+ TextView navTouchpad = view.requireViewById(R.id.nav_touchpad);
+ navTouchpad.setOnTouchListener(
+ (v, event) -> {
+ mInputManager.sendInputEventToFocusedDisplay(
+ InputDeviceType.DEVICE_TYPE_NAVIGATION_TOUCHPAD, event);
+ return true;
+ });
+ return view;
+ }
+}
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VdmClientApplication.java
similarity index 69%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VdmClientApplication.java
index dd03545..f031355 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VdmClientApplication.java
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+package com.example.android.vdmdemo.client;
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+import android.app.Application;
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+import dagger.hilt.android.HiltAndroidApp;
+
+/** VDM Client application class. */
+@HiltAndroidApp(Application.class)
+public class VdmClientApplication extends Hilt_VdmClientApplication {}
diff --git a/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VirtualSensorController.java b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VirtualSensorController.java
new file mode 100644
index 0000000..3157f09
--- /dev/null
+++ b/samples/VirtualDeviceManager/client/src/com/example/android/vdmdemo/client/VirtualSensorController.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.client;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteSensorEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.SensorCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.SensorConfiguration;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.google.common.primitives.Floats;
+
+import dagger.hilt.android.qualifiers.ApplicationContext;
+import dagger.hilt.android.scopes.ActivityScoped;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+@ActivityScoped
+final class VirtualSensorController implements AutoCloseable {
+
+ private final RemoteIo mRemoteIo;
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+ private final SensorManager mSensorManager;
+ private final HandlerThread mListenerThread;
+ private final Handler mHandler;
+
+ private final SensorEventListener mSensorEventListener =
+ new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setSensorEvent(
+ RemoteSensorEvent.newBuilder()
+ .setSensorType(event.sensor.getType())
+ .addAllValues(Floats.asList(event.values)))
+ .build());
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+ };
+
+ @Inject
+ VirtualSensorController(@ApplicationContext Context context, RemoteIo remoteIo) {
+ mSensorManager = context.getSystemService(SensorManager.class);
+ mRemoteIo = remoteIo;
+
+ mListenerThread = new HandlerThread("VirtualSensorListener");
+ mListenerThread.start();
+ mHandler = new Handler(mListenerThread.getLooper());
+
+ remoteIo.addMessageConsumer(mRemoteEventConsumer);
+ }
+
+ @Override
+ public void close() {
+ mSensorManager.unregisterListener(mSensorEventListener);
+ mListenerThread.quitSafely();
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ }
+
+ public List<SensorCapabilities> getSensorCapabilities() {
+ return mSensorManager.getSensorList(Sensor.TYPE_ALL).stream()
+ .map(
+ sensor ->
+ SensorCapabilities.newBuilder()
+ .setType(sensor.getType())
+ .setName(sensor.getName())
+ .setVendor(sensor.getVendor())
+ .setMaxRange(sensor.getMaximumRange())
+ .setResolution(sensor.getResolution())
+ .setPower(sensor.getPower())
+ .setMinDelayUs(sensor.getMinDelay())
+ .setMaxDelayUs(sensor.getMaxDelay())
+ .build())
+ .collect(Collectors.toList());
+ }
+
+ private void processRemoteEvent(RemoteEvent remoteEvent) {
+ if (!remoteEvent.hasSensorConfiguration()) {
+ return;
+ }
+ SensorConfiguration config = remoteEvent.getSensorConfiguration();
+ Sensor sensor = mSensorManager.getDefaultSensor(config.getSensorType());
+ if (sensor == null) {
+ return;
+ }
+ if (config.getEnabled()) {
+ mSensorManager.registerListener(
+ mSensorEventListener,
+ sensor,
+ config.getSamplingPeriodUs(),
+ config.getBatchReportingLatencyUs(),
+ mHandler);
+ } else {
+ mSensorManager.unregisterListener(mSensorEventListener, sensor);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/common/AndroidManifest.xml b/samples/VirtualDeviceManager/common/AndroidManifest.xml
new file mode 100644
index 0000000..49664cd
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.vdmdemo.common"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:minSdkVersion="33" />
+
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+ android:usesPermissionFlags="neverForLocation" />
+
+</manifest>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/common/proto/remote_event.proto b/samples/VirtualDeviceManager/common/proto/remote_event.proto
new file mode 100644
index 0000000..12750f6
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/proto/remote_event.proto
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+syntax = "proto3";
+
+package com.example.android.vdmdemo.common;
+
+option java_outer_classname = "RemoteEventProto";
+option java_package = "com.example.android.vdmdemo.common";
+
+// Next ID: 16
+message RemoteEvent {
+ int32 display_id = 1;
+
+ oneof event {
+ DeviceCapabilities device_capabilities = 2;
+ StartStreaming start_streaming = 3;
+ StopStreaming stop_streaming = 4;
+ DisplayCapabilities display_capabilities = 5;
+ DisplayRotation display_rotation = 6;
+ DisplayFrame display_frame = 7;
+ SensorConfiguration sensor_configuration = 8;
+ RemoteInputEvent input_event = 9;
+ RemoteSensorEvent sensor_event = 10;
+ StartAudio start_audio = 11;
+ AudioFrame audio_frame = 12;
+ StopAudio stop_audio = 13;
+ RemoteHomeEvent home_event = 14;
+ DisplayChangeEvent display_change_event = 15;
+ }
+}
+
+// TODO(b/289897950): Support virtual audio input and output
+message DeviceCapabilities {
+ string device_name = 1;
+ repeated SensorCapabilities sensor_capabilities = 2;
+}
+
+message DisplayRotation {
+ int32 rotation_degrees = 1;
+}
+
+message SensorCapabilities {
+ int32 type = 1;
+ string name = 2;
+ string vendor = 3;
+ float max_range = 4;
+ float resolution = 5;
+ float power = 6;
+ int32 min_delay_us = 7;
+ int32 max_delay_us = 8;
+}
+
+message StartStreaming {
+ bool home_enabled = 1;
+}
+
+message StopStreaming {
+ bool pause = 1;
+}
+
+message DisplayCapabilities {
+ int32 viewport_width = 1;
+ int32 viewport_height = 2;
+ int32 density_dpi = 3;
+}
+
+message DisplayFrame {
+ bytes frame_data = 1;
+ int32 frame_index = 2;
+ int32 flags = 3;
+ int64 presentation_time_us = 4;
+}
+
+enum InputDeviceType {
+ DEVICE_TYPE_NONE = 0;
+ DEVICE_TYPE_MOUSE = 1;
+ DEVICE_TYPE_DPAD = 2;
+ DEVICE_TYPE_NAVIGATION_TOUCHPAD = 3;
+ DEVICE_TYPE_TOUCHSCREEN = 4;
+ DEVICE_TYPE_KEYBOARD = 5;
+}
+
+message RemoteInputEvent {
+ int64 timestamp_ms = 1;
+
+ InputDeviceType device_type = 2;
+
+ oneof event {
+ RemoteMotionEvent mouse_relative_event = 3;
+ RemoteKeyEvent mouse_button_event = 4;
+ RemoteMotionEvent mouse_scroll_event = 5;
+
+ RemoteKeyEvent key_event = 6;
+
+ RemoteMotionEvent touch_event = 7;
+ }
+}
+
+message RemoteMotionEvent {
+ int32 pointer_id = 1;
+ int32 action = 2;
+ float x = 3;
+ float y = 4;
+ float pressure = 5;
+}
+
+message RemoteKeyEvent {
+ int32 action = 1;
+ int32 key_code = 2;
+}
+
+message SensorConfiguration {
+ int32 sensor_type = 1;
+ bool enabled = 2;
+ int32 sampling_period_us = 3;
+ int32 batch_reporting_latency_us = 4;
+}
+
+message RemoteSensorEvent {
+ int32 sensor_type = 1;
+ repeated float values = 2;
+}
+
+message StartAudio {}
+
+message StopAudio {}
+
+message AudioFrame {
+ bytes data = 1;
+}
+
+message RemoteHomeEvent {}
+
+message DisplayChangeEvent {
+ string title = 1;
+}
diff --git a/samples/VirtualDeviceManager/common/res/layout/connectivity_fragment.xml b/samples/VirtualDeviceManager/common/res/layout/connectivity_fragment.xml
new file mode 100644
index 0000000..919743c
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/res/layout/connectivity_fragment.xml
@@ -0,0 +1,6 @@
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/connection_status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:padding="5dp" />
diff --git a/samples/VirtualDeviceManager/common/res/values/strings.xml b/samples/VirtualDeviceManager/common/res/values/strings.xml
new file mode 100644
index 0000000..84b6605
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/res/values/strings.xml
@@ -0,0 +1,8 @@
+<resources>
+ <string name="this_device" translatable="false">%s: %s</string>
+ <string name="disconnected" translatable="false">Disconnected</string>
+ <string name="initialized" translatable="false">Looking for devices...</string>
+ <string name="connecting" translatable="false">Connecting to %s...</string>
+ <string name="connected" translatable="false">Connected to %s</string>
+ <string name="error" translatable="false">Error: %s</string>
+</resources>
diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java
new file mode 100644
index 0000000..c1c251d
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.common;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.wifi.aware.AttachCallback;
+import android.net.wifi.aware.DiscoverySession;
+import android.net.wifi.aware.DiscoverySessionCallback;
+import android.net.wifi.aware.PeerHandle;
+import android.net.wifi.aware.PublishConfig;
+import android.net.wifi.aware.PublishDiscoverySession;
+import android.net.wifi.aware.SubscribeConfig;
+import android.net.wifi.aware.SubscribeDiscoverySession;
+import android.net.wifi.aware.WifiAwareManager;
+import android.net.wifi.aware.WifiAwareNetworkInfo;
+import android.net.wifi.aware.WifiAwareNetworkSpecifier;
+import android.net.wifi.aware.WifiAwareSession;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import dagger.hilt.android.qualifiers.ApplicationContext;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Shared class between the client and the host, managing the connection between them. */
+@Singleton
+public class ConnectionManager {
+
+ private static final String TAG = "VdmConnectionManager";
+ private static final String CONNECTION_SERVICE_ID = "com.example.android.vdmdemo";
+
+ private final RemoteIo mRemoteIo;
+
+ @ApplicationContext private final Context mContext;
+ private final ConnectivityManager mConnectivityManager;
+ private final Handler mBackgroundHandler;
+ private final Object mSessionLock = new Object();
+
+ private CompletableFuture<WifiAwareSession> mWifiAwareSessionFuture = new CompletableFuture<>();
+
+ @GuardedBy("mSessionLock")
+ private DiscoverySession mDiscoverySession;
+
+ @GuardedBy("mSessionLock")
+ private boolean mDiscoverySessionInitiated = false;
+
+ /** Simple data structure to allow clients to query the current status. */
+ public static final class ConnectionStatus {
+ public String remoteDeviceName = null;
+ public boolean connected = false;
+ }
+
+ @GuardedBy("mSessionLock")
+ private final ConnectionStatus mConnectionStatus = new ConnectionStatus();
+
+ /** Simple callback to notify connection and disconnection events. */
+ public interface ConnectionCallback {
+ /** The device is ready for connecting to other devices. */
+ default void onInitialized() {}
+
+ /** A connection has been initiated. */
+ default void onConnecting(String remoteDeviceName) {}
+
+ /** A connection has been established. */
+ default void onConnected(String remoteDeviceName) {}
+
+ /** The connection has been lost. */
+ default void onDisconnected() {}
+
+ /** An unrecoverable error has occurred. */
+ default void onError(String message) {}
+ }
+
+ @GuardedBy("mConnectionCallbacks")
+ private final List<ConnectionCallback> mConnectionCallbacks = new ArrayList<>();
+
+ private final RemoteIo.StreamClosedCallback mStreamClosedCallback = this::disconnect;
+
+ @Inject
+ ConnectionManager(@ApplicationContext Context context, RemoteIo remoteIo) {
+ mRemoteIo = remoteIo;
+ mContext = context;
+
+ mConnectivityManager = context.getSystemService(ConnectivityManager.class);
+ final HandlerThread backgroundThread = new HandlerThread("ConnectionThread");
+ backgroundThread.start();
+ mBackgroundHandler = new Handler(backgroundThread.getLooper());
+ }
+
+ static String getLocalEndpointId() {
+ return Build.MODEL;
+ }
+
+ /** Registers a listener for connection events. */
+ public void addConnectionCallback(ConnectionCallback callback) {
+ synchronized (mConnectionCallbacks) {
+ mConnectionCallbacks.add(callback);
+ }
+ }
+
+ /** Registers a listener for connection events. */
+ public void removeConnectionCallback(ConnectionCallback callback) {
+ synchronized (mConnectionCallbacks) {
+ mConnectionCallbacks.remove(callback);
+ }
+ }
+
+ /** Returns the current connection status. */
+ public ConnectionStatus getConnectionStatus() {
+ synchronized (mSessionLock) {
+ return mConnectionStatus;
+ }
+ }
+
+ /** Publish a local service so remote devices can discover this device. */
+ public void startHostSession() {
+ synchronized (mSessionLock) {
+ if (mDiscoverySessionInitiated) {
+ Log.d(TAG, "Session already initiated, ignoring new session request.");
+ return;
+ }
+ mDiscoverySessionInitiated = true;
+ }
+ var unused = createSession().thenAccept(session -> session.publish(
+ new PublishConfig.Builder().setServiceName(CONNECTION_SERVICE_ID).build(),
+ new HostDiscoverySessionCallback(),
+ mBackgroundHandler));
+ }
+
+ /** Looks for published services from remote devices and subscribes to them. */
+ public void startClientSession() {
+ synchronized (mSessionLock) {
+ if (mDiscoverySessionInitiated) {
+ Log.d(TAG, "Session already initiated, ignoring new session request.");
+ return;
+ }
+ mDiscoverySessionInitiated = true;
+ }
+ var unused = createSession().thenAccept(session -> session.subscribe(
+ new SubscribeConfig.Builder().setServiceName(CONNECTION_SERVICE_ID).build(),
+ new ClientDiscoverySessionCallback(),
+ mBackgroundHandler));
+ }
+
+ private boolean isConnected() {
+ synchronized (mSessionLock) {
+ return mConnectionStatus.connected;
+ }
+ }
+
+ private CompletableFuture<WifiAwareSession> createSession() {
+ if (mWifiAwareSessionFuture.isDone()
+ && !mWifiAwareSessionFuture.isCompletedExceptionally()) {
+ return mWifiAwareSessionFuture;
+ }
+
+ Log.d(TAG, "Creating a new Wifi Aware session.");
+ WifiAwareManager wifiAwareManager = mContext.getSystemService(WifiAwareManager.class);
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
+ || wifiAwareManager == null
+ || !wifiAwareManager.isAvailable()) {
+ mWifiAwareSessionFuture.completeExceptionally(
+ new Exception("Wifi Aware is not available."));
+ } else {
+ wifiAwareManager.attach(
+ new AttachCallback() {
+ @Override
+ public void onAttached(WifiAwareSession session) {
+ Log.d(TAG, "New Wifi Aware attached.");
+ mWifiAwareSessionFuture.complete(session);
+ }
+
+ @Override
+ public void onAttachFailed() {
+ mWifiAwareSessionFuture.completeExceptionally(
+ new Exception("Failed to attach Wifi Aware session."));
+ }
+ },
+ mBackgroundHandler);
+ }
+ mWifiAwareSessionFuture = mWifiAwareSessionFuture
+ .exceptionally(e -> {
+ Log.e(TAG, "Failed to create Wifi Aware session", e);
+ onError("Failed to create Wifi Aware session");
+ return null;
+ });
+ return mWifiAwareSessionFuture;
+ }
+
+ /** Explicitly terminate any existing connection. */
+ public void disconnect() {
+ Log.d(TAG, "Terminating connections.");
+ synchronized (mSessionLock) {
+ if (mDiscoverySession != null) {
+ Log.d(TAG, "Closing existing discovery session.");
+ mDiscoverySession.close();
+ mDiscoverySession = null;
+ mDiscoverySessionInitiated = false;
+ }
+ mConnectionStatus.remoteDeviceName = null;
+ mConnectionStatus.connected = false;
+ synchronized (mConnectionCallbacks) {
+ for (ConnectionCallback callback : mConnectionCallbacks) {
+ callback.onDisconnected();
+ }
+ }
+ }
+ }
+
+ private void onSocketAvailable(Socket socket) throws IOException {
+ mRemoteIo.initialize(socket.getInputStream(), mStreamClosedCallback);
+ mRemoteIo.initialize(socket.getOutputStream(), mStreamClosedCallback);
+ synchronized (mSessionLock) {
+ mConnectionStatus.connected = true;
+ synchronized (mConnectionCallbacks) {
+ for (ConnectionCallback callback : mConnectionCallbacks) {
+ callback.onConnected(mConnectionStatus.remoteDeviceName);
+ }
+ }
+ }
+ }
+
+ private void onInitialized() {
+ Log.d(TAG, "Discovery session initialized.");
+ synchronized (mConnectionCallbacks) {
+ for (ConnectionCallback callback : mConnectionCallbacks) {
+ callback.onInitialized();
+ }
+ }
+ }
+
+ private void onError(String message) {
+ Log.e(TAG, "Error: " + message);
+ synchronized (mConnectionCallbacks) {
+ for (ConnectionCallback callback : mConnectionCallbacks) {
+ callback.onError(message);
+ }
+ }
+ }
+
+ private class VdmDiscoverySessionCallback extends DiscoverySessionCallback {
+
+ @GuardedBy("mSessionLock")
+ private NetworkCallback mNetworkCallback;
+
+ @Override
+ public void onSessionTerminated() {
+ Log.d(TAG, "Discovery session terminated.");
+ synchronized (mSessionLock) {
+ if (mNetworkCallback != null) {
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
+ }
+ }
+ }
+
+ void sendLocalEndpointId(PeerHandle peerHandle) {
+ synchronized (mSessionLock) {
+ mDiscoverySession.sendMessage(peerHandle, 0, getLocalEndpointId().getBytes());
+ }
+ }
+
+ void onConnecting(byte[] remoteDeviceName) {
+ synchronized (mSessionLock) {
+ mConnectionStatus.remoteDeviceName = new String(remoteDeviceName);
+ Log.d(TAG, "Connecting to " + mConnectionStatus.remoteDeviceName);
+ synchronized (mConnectionCallbacks) {
+ for (ConnectionCallback callback : mConnectionCallbacks) {
+ callback.onConnecting(mConnectionStatus.remoteDeviceName);
+ }
+ }
+ }
+ }
+
+ @GuardedBy("mSessionLock")
+ void requestNetworkLocked(
+ PeerHandle peerHandle, Optional<Integer> port, NetworkCallback networkCallback) {
+ WifiAwareNetworkSpecifier.Builder networkSpecifierBuilder =
+ new WifiAwareNetworkSpecifier.Builder(mDiscoverySession, peerHandle)
+ .setPskPassphrase(CONNECTION_SERVICE_ID);
+ port.ifPresent(networkSpecifierBuilder::setPort);
+
+ NetworkRequest networkRequest =
+ new NetworkRequest.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
+ .setNetworkSpecifier(networkSpecifierBuilder.build())
+ .build();
+ mNetworkCallback = networkCallback;
+ Log.d(TAG, "Requesting network");
+ mConnectivityManager.requestNetwork(networkRequest, mNetworkCallback);
+ }
+ }
+
+ private final class HostDiscoverySessionCallback extends VdmDiscoverySessionCallback {
+
+ @Override
+ public void onPublishStarted(@NonNull PublishDiscoverySession session) {
+ synchronized (mSessionLock) {
+ mDiscoverySession = session;
+ onInitialized();
+ }
+ }
+
+ @Override
+ public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
+ Log.d(TAG, "Received message: " + new String(message));
+ synchronized (mSessionLock) {
+ if (isConnected()) {
+ return;
+ }
+
+ onConnecting(message);
+
+ try {
+ ServerSocket serverSocket = new ServerSocket(0);
+ requestNetworkLocked(
+ peerHandle,
+ Optional.of(serverSocket.getLocalPort()),
+ new NetworkCallback());
+ sendLocalEndpointId(peerHandle);
+ onSocketAvailable(serverSocket.accept());
+ } catch (IOException e) {
+ onError("Failed to establish connection.");
+ }
+ }
+ }
+ }
+
+ private final class ClientDiscoverySessionCallback extends VdmDiscoverySessionCallback {
+
+ @Override
+ public void onSubscribeStarted(@NonNull SubscribeDiscoverySession session) {
+ synchronized (mSessionLock) {
+ mDiscoverySession = session;
+ onInitialized();
+ }
+ }
+
+ @Override
+ public void onServiceDiscovered(
+ PeerHandle peerHandle, byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
+ Log.d(TAG, "Discovered service: " + new String(serviceSpecificInfo));
+ sendLocalEndpointId(peerHandle);
+ }
+
+ @Override
+ public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
+ Log.d(TAG, "Received message: " + new String(message));
+ synchronized (mSessionLock) {
+ if (isConnected()) {
+ return;
+ }
+ onConnecting(message);
+ requestNetworkLocked(
+ peerHandle, /* port= */ Optional.empty(), new ClientNetworkCallback());
+ }
+ }
+ }
+
+ private class NetworkCallback extends ConnectivityManager.NetworkCallback {
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ disconnect();
+ }
+ }
+
+ private class ClientNetworkCallback extends NetworkCallback {
+
+ @Override
+ public void onCapabilitiesChanged(@NonNull Network network,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ if (isConnected()) {
+ return;
+ }
+
+ WifiAwareNetworkInfo peerAwareInfo =
+ (WifiAwareNetworkInfo) networkCapabilities.getTransportInfo();
+ Inet6Address peerIpv6 = peerAwareInfo.getPeerIpv6Addr();
+ int peerPort = peerAwareInfo.getPort();
+ try {
+ Socket socket = network.getSocketFactory().createSocket(peerIpv6, peerPort);
+ onSocketAvailable(socket);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to establish connection.", e);
+ onError("Failed to establish connection.");
+ }
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectivityFragment.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectivityFragment.java
new file mode 100644
index 0000000..10f8a01
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectivityFragment.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.common;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/** Fragment that holds the connectivity status UI. */
+@AndroidEntryPoint(Fragment.class)
+public final class ConnectivityFragment extends Hilt_ConnectivityFragment {
+
+ @Inject ConnectionManager mConnectionManager;
+
+ private TextView mStatus = null;
+ private int mDefaultBackgroundColor;
+
+ private final ConnectionManager.ConnectionCallback mConnectionCallback =
+ new ConnectionManager.ConnectionCallback() {
+ @Override
+ public void onInitialized() {
+ updateStatus(mDefaultBackgroundColor, R.string.initialized);
+ }
+
+ @Override
+ public void onConnecting(String remoteDeviceName) {
+ updateStatus(mDefaultBackgroundColor, R.string.connecting, remoteDeviceName);
+ }
+
+ @Override
+ public void onConnected(String remoteDeviceName) {
+ updateStatus(Color.GREEN, R.string.connected, remoteDeviceName);
+ }
+
+ @Override
+ public void onDisconnected() {
+ updateStatus(mDefaultBackgroundColor, R.string.disconnected);
+ }
+
+ @Override
+ public void onError(String message) {
+ updateStatus(Color.RED, R.string.error, message);
+ }
+ };
+
+ public ConnectivityFragment() {
+ super(R.layout.connectivity_fragment);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, Bundle bundle) {
+ super.onViewCreated(view, bundle);
+
+ mStatus = requireActivity().requireViewById(R.id.connection_status);
+
+ TypedValue background = new TypedValue();
+ getActivity()
+ .getTheme()
+ .resolveAttribute(android.R.attr.windowBackground, background, true);
+ mDefaultBackgroundColor = background.isColorType() ? background.data : Color.WHITE;
+
+ CharSequence currentTitle = getActivity().getTitle();
+ String localEndpointId = ConnectionManager.getLocalEndpointId();
+ String title = getActivity().getString(R.string.this_device, currentTitle, localEndpointId);
+ getActivity().setTitle(title);
+
+ ConnectionManager.ConnectionStatus connectionStatus =
+ mConnectionManager.getConnectionStatus();
+ if (connectionStatus.connected) {
+ mConnectionCallback.onConnected(connectionStatus.remoteDeviceName);
+ } else if (connectionStatus.remoteDeviceName != null) {
+ mConnectionCallback.onConnecting(connectionStatus.remoteDeviceName);
+ } else {
+ mConnectionCallback.onDisconnected();
+ }
+
+ mConnectionManager.addConnectionCallback(mConnectionCallback);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mConnectionManager.removeConnectionCallback(mConnectionCallback);
+ }
+
+ private void updateStatus(int backgroundColor, int resId, Object... formatArgs) {
+ Activity activity = requireActivity();
+ activity.runOnUiThread(() -> {
+ mStatus.setText(activity.getString(resId, formatArgs));
+ mStatus.setBackgroundColor(backgroundColor);
+ });
+ }
+}
diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java
new file mode 100644
index 0000000..71a62d8
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.common;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Simple message exchange framework between the client and the host. */
+@Singleton
+public class RemoteIo {
+ public static final String TAG = "VdmRemoteIo";
+
+ interface StreamClosedCallback {
+ void onStreamClosed();
+ }
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private OutputStream mOutputStream = null;
+
+ private StreamClosedCallback mOutputStreamClosedCallback = null;
+ private final Handler mSendMessageHandler;
+
+ @GuardedBy("mMessageConsumers")
+ private final Map<Object, MessageConsumer> mMessageConsumers = new ArrayMap<>();
+
+ @Inject
+ RemoteIo() {
+ final HandlerThread sendMessageThread = new HandlerThread("SendMessageThread");
+ sendMessageThread.start();
+ mSendMessageHandler = new Handler(sendMessageThread.getLooper());
+ }
+
+ @SuppressWarnings("ThreadPriorityCheck")
+ void initialize(InputStream inputStream, StreamClosedCallback inputStreamClosedCallback) {
+ Thread t = new Thread(new ReceiverRunnable(inputStream, inputStreamClosedCallback));
+ t.setPriority(Thread.MAX_PRIORITY);
+ t.start();
+ }
+
+ void initialize(
+ OutputStream outputStream, StreamClosedCallback outputStreamClosedCallback) {
+ synchronized (mLock) {
+ mOutputStream = outputStream;
+ mOutputStreamClosedCallback = outputStreamClosedCallback;
+ }
+ }
+
+ /** Registers a consumer for processing events coming from the remote device. */
+ public void addMessageConsumer(Consumer<RemoteEvent> consumer) {
+ synchronized (mMessageConsumers) {
+ mMessageConsumers.put(consumer, new MessageConsumer(consumer));
+ }
+ }
+
+ /** Unregisters a previously registered message consumer. */
+ public void removeMessageConsumer(Consumer<RemoteEvent> consumer) {
+ synchronized (mMessageConsumers) {
+ if (mMessageConsumers.remove(consumer) == null) {
+ Log.w(TAG, "Failed to remove message consumer.");
+ }
+ }
+ }
+
+ /** Sends an event to the remote device. */
+ public void sendMessage(RemoteEvent event) {
+ synchronized (mLock) {
+ if (mOutputStream == null) {
+ Log.e(TAG, "Failed to send event, RemoteIO not initialized.");
+ return;
+ }
+ }
+ mSendMessageHandler.post(() -> {
+ synchronized (mLock) {
+ try {
+ event.writeDelimitedTo(mOutputStream);
+ mOutputStream.flush();
+ } catch (IOException e) {
+ mOutputStream = null;
+ mOutputStreamClosedCallback.onStreamClosed();
+ }
+ }
+ });
+ }
+
+ private class ReceiverRunnable implements Runnable {
+
+ private final InputStream mInputStream;
+ private final StreamClosedCallback mInputStreamClosedCallback;
+
+ ReceiverRunnable(InputStream inputStream, StreamClosedCallback inputStreamClosedCallback) {
+ mInputStream = inputStream;
+ mInputStreamClosedCallback = inputStreamClosedCallback;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ RemoteEvent event = RemoteEvent.parseDelimitedFrom(mInputStream);
+ if (event == null) {
+ break;
+ }
+ synchronized (mMessageConsumers) {
+ mMessageConsumers.values().forEach(consumer -> consumer.accept(event));
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to obtain event: " + e);
+ }
+ mInputStreamClosedCallback.onStreamClosed();
+ }
+ }
+
+ private static class MessageConsumer {
+ private final Executor mExecutor;
+ private final Consumer<RemoteEvent> mConsumer;
+
+ MessageConsumer(Consumer<RemoteEvent> consumer) {
+ mExecutor = Executors.newSingleThreadExecutor();
+ mConsumer = consumer;
+ }
+
+ public void accept(RemoteEvent event) {
+ mExecutor.execute(() -> mConsumer.accept(event));
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/VideoManager.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/VideoManager.java
new file mode 100644
index 0000000..3df3566
--- /dev/null
+++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/VideoManager.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.common;
+
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.DisplayFrame;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.google.common.util.concurrent.Uninterruptibles;
+import com.google.protobuf.ByteString;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+/** Shared class between the client and the host, managing the video encoding and decoding. */
+public class VideoManager {
+ private static final String TAG = "VideoManager";
+ private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+
+ @GuardedBy("mCodecLock")
+ private MediaCodec mMediaCodec;
+
+ private final Object mCodecLock = new Object();
+ private final HandlerThread mCallbackThread;
+ private final boolean mRecordEncoderOutput;
+ private final BlockingQueue<RemoteEvent> mEventQueue = new LinkedBlockingQueue<>(100);
+ private final BlockingQueue<Integer> mFreeInputBuffers = new LinkedBlockingQueue<>(100);
+ private final RemoteIo mRemoteIo;
+ private final Consumer<RemoteEvent> mRemoteFrameConsumer = this::processFrameProto;
+ private final int mDisplayId;
+ private int mFrameIndex = 0;
+ private StorageFile mStorageFile;
+ private DecoderThread mDecoderThread;
+
+ private VideoManager(
+ int displayId, RemoteIo remoteIo, MediaCodec mediaCodec, boolean recordEncoderOutput) {
+ mDisplayId = displayId;
+ mRemoteIo = remoteIo;
+ mMediaCodec = mediaCodec;
+ mRecordEncoderOutput = recordEncoderOutput;
+
+ mCallbackThread = new HandlerThread("VideoManager-" + displayId);
+ mCallbackThread.start();
+ mediaCodec.setCallback(new MediaCodecCallback(), new Handler(mCallbackThread.getLooper()));
+
+ if (!mediaCodec.getCodecInfo().isEncoder()) {
+ remoteIo.addMessageConsumer(mRemoteFrameConsumer);
+ }
+
+ if (recordEncoderOutput) {
+ mStorageFile = new StorageFile(displayId);
+ }
+ }
+
+ /** Creates a VideoManager instance for encoding. */
+ public static VideoManager createEncoder(
+ int displayId, RemoteIo remoteIo, boolean recordEncoderOutput) {
+ try {
+ MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
+ return new VideoManager(displayId, remoteIo, mediaCodec, recordEncoderOutput);
+ } catch (IOException e) {
+ throw new AssertionError("Unhandled exception", e);
+ }
+ }
+
+ /** Creates a VideoManager instance for decoding. */
+ public static VideoManager createDecoder(int displayId, RemoteIo remoteIo) {
+ try {
+ MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
+ return new VideoManager(displayId, remoteIo, mediaCodec, false);
+ } catch (IOException e) {
+ throw new AssertionError("Unhandled exception", e);
+ }
+ }
+
+ /** Stops processing and resets the internal state. */
+ public void stop() {
+ synchronized (mCodecLock) {
+ if (mMediaCodec == null) {
+ return;
+ }
+ if (mMediaCodec.getCodecInfo().isEncoder()) {
+ mMediaCodec.signalEndOfInputStream();
+ } else {
+ mRemoteIo.removeMessageConsumer(mRemoteFrameConsumer);
+ mEventQueue.clear();
+ mDecoderThread.exit();
+ }
+ mCallbackThread.quitSafely();
+ mMediaCodec.flush();
+ mMediaCodec.stop();
+ mMediaCodec.release();
+ mMediaCodec = null;
+ }
+ if (mRecordEncoderOutput) {
+ mStorageFile.closeOutputFile();
+ }
+ }
+
+ /** Creates a surface for encoding. */
+ public Surface createInputSurface(int width, int height, int frameRate) {
+ MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
+ mediaFormat.setInteger(
+ MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
+ mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500000);
+ mediaFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 0);
+ mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
+ mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
+ synchronized (mCodecLock) {
+ mMediaCodec.configure(
+ mediaFormat, /* surface= */ null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+ return mMediaCodec.createInputSurface();
+ }
+ }
+
+ /** Starts encoding. {@link #createInputSurface} must have been called already. */
+ public void startEncoding() {
+ synchronized (mCodecLock) {
+ mMediaCodec.start();
+ }
+ }
+
+ /** Starts decoding from the given surface. */
+ public void startDecoding(Surface surface, int width, int height) {
+ MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
+ mediaFormat.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
+ mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 100);
+ synchronized (mCodecLock) {
+ mMediaCodec.configure(mediaFormat, surface, null, 0);
+ mMediaCodec.start();
+ }
+ mDecoderThread = new DecoderThread();
+ mDecoderThread.start();
+ }
+
+ private RemoteEvent createFrameProto(byte[] data, int flags, long presentationTimeUs) {
+ return RemoteEvent.newBuilder()
+ .setDisplayId(mDisplayId)
+ .setDisplayFrame(
+ DisplayFrame.newBuilder()
+ .setFrameData(ByteString.copyFrom(data))
+ .setFrameIndex(mFrameIndex++)
+ .setPresentationTimeUs(presentationTimeUs)
+ .setFlags(flags))
+ .build();
+ }
+
+ private void processFrameProto(RemoteEvent event) {
+ if (event.hasDisplayFrame() && event.getDisplayId() == mDisplayId) {
+ Uninterruptibles.putUninterruptibly(mEventQueue, event);
+ }
+ }
+
+ private final class MediaCodecCallback extends MediaCodec.Callback {
+ @Override
+ public void onInputBufferAvailable(@NonNull MediaCodec codec, int i) {
+ mFreeInputBuffers.add(i);
+ }
+
+ @Override
+ public void onOutputBufferAvailable(
+ @NonNull MediaCodec codec, int i, @NonNull BufferInfo bufferInfo) {
+ synchronized (mCodecLock) {
+ if (mMediaCodec == null) {
+ return;
+ }
+ if (mMediaCodec.getCodecInfo().isEncoder()) {
+ ByteBuffer buffer = mMediaCodec.getOutputBuffer(i);
+ byte[] data = new byte[bufferInfo.size];
+ Objects.requireNonNull(buffer).get(data, bufferInfo.offset, bufferInfo.size);
+ mMediaCodec.releaseOutputBuffer(i, false);
+ if (mRecordEncoderOutput) {
+ mStorageFile.writeOutputFile(data);
+ }
+
+ mRemoteIo.sendMessage(
+ createFrameProto(
+ data, bufferInfo.flags, bufferInfo.presentationTimeUs));
+ } else {
+ mMediaCodec.releaseOutputBuffer(i, true);
+ }
+ }
+ }
+
+ @Override
+ public void onError(@NonNull MediaCodec mediaCodec, @NonNull CodecException e) {}
+
+ @Override
+ public void onOutputFormatChanged(
+ @NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {}
+ }
+
+ private class DecoderThread extends Thread {
+
+ private final AtomicBoolean mExit = new AtomicBoolean(false);
+
+ @SuppressWarnings("Interruption")
+ void exit() {
+ mExit.set(true);
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ while (!(Thread.interrupted() && mExit.get())) {
+ try {
+ RemoteEvent event = mEventQueue.take();
+ int inputBuffer = mFreeInputBuffers.take();
+
+ synchronized (mCodecLock) {
+ if (mMediaCodec == null) {
+ continue;
+ }
+ ByteBuffer inBuffer = mMediaCodec.getInputBuffer(inputBuffer);
+ byte[] data = event.getDisplayFrame().getFrameData().toByteArray();
+ Objects.requireNonNull(inBuffer).put(data);
+ if (mRecordEncoderOutput) {
+ mStorageFile.writeOutputFile(data);
+ }
+ mMediaCodec.queueInputBuffer(
+ inputBuffer,
+ 0,
+ event.getDisplayFrame().getFrameData().size(),
+ event.getDisplayFrame().getPresentationTimeUs(),
+ event.getDisplayFrame().getFlags());
+ }
+ } catch (InterruptedException e) {
+ if (mExit.get()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private static class StorageFile {
+ private static final String DIR = "Download";
+ private static final String FILENAME = "vdmdemo_encoder_output";
+
+ private OutputStream mOutputStream;
+
+ private StorageFile(int displayId) {
+ String filePath = DIR + "/" + FILENAME + "_" + displayId + ".h264";
+ File f = new File(Environment.getExternalStorageDirectory(), filePath);
+ try {
+ mOutputStream = new BufferedOutputStream(new FileOutputStream(f));
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Error creating or opening storage file", e);
+ }
+ }
+
+ private void writeOutputFile(byte[] data) {
+ if (mOutputStream == null) {
+ return;
+ }
+ try {
+ mOutputStream.write(data);
+ } catch (IOException e) {
+ Log.e(TAG, "Error writing to output file", e);
+ }
+ }
+
+ private void closeOutputFile() {
+ if (mOutputStream == null) {
+ return;
+ }
+ try {
+ mOutputStream.flush();
+ mOutputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing output file", e);
+ }
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/AndroidManifest.xml b/samples/VirtualDeviceManager/demos/AndroidManifest.xml
new file mode 100644
index 0000000..f13f887
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/AndroidManifest.xml
@@ -0,0 +1,76 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.vdmdemo.demos"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="34"
+ android:targetSdkVersion="35" />
+
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.READ_CALENDAR" />
+ <uses-permission android:name="android.permission.READ_SMS" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.BODY_SENSORS" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATION" />
+ <uses-permission android:name="android.permission.VIBRATE" />
+
+ <!-- LINT.IfChange -->
+ <application
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat.NoActionBar">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:label="VDM Demos">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".SensorDemoActivity"
+ android:exported="false"
+ android:label="@string/sensor_demo" />
+
+ <activity
+ android:name=".RotationDemoActivity"
+ android:exported="true"
+ android:label="@string/rotation_demo" />
+
+ <activity
+ android:name=".HomeDemoActivity"
+ android:exported="true"
+ android:label="@string/home_demo" />
+
+ <activity
+ android:name=".PermissionsDemoActivity"
+ android:exported="true"
+ android:label="@string/permissions_demo" />
+
+ <activity
+ android:name=".SecureWindowDemoActivity"
+ android:exported="true"
+ android:label="@string/secure_window_demo" />
+
+ <activity
+ android:name=".LatencyDemoActivity"
+ android:exported="true"
+ android:label="@string/latency_demo" />
+
+ <activity
+ android:name=".VibrationDemoActivity"
+ android:exported="true"
+ android:label="@string/vibration_demo" />
+
+ </application>
+ <!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:demos) -->
+</manifest>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/home_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/home_demo_activity.xml
new file mode 100644
index 0000000..4841802
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/home_demo_activity.xml
@@ -0,0 +1,23 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <Button
+ android:id="@+id/send_home"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onSendHomeIntent"
+ android:text="@string/send_home" />
+
+ <Button
+ android:id="@+id/send_secondary_home"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onSendSecondaryHomeIntent"
+ android:text="@string/send_secondary_home" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/latency_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/latency_demo_activity.xml
new file mode 100644
index 0000000..bca3486
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/latency_demo_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <com.example.android.vdmdemo.demos.CounterView
+ android:id="@+id/counter"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/main_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/main_activity.xml
new file mode 100644
index 0000000..c1553b6
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/main_activity.xml
@@ -0,0 +1,58 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <Button
+ android:id="@+id/sensor_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/sensor_demo" />
+
+ <Button
+ android:id="@+id/vibration_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/vibration_demo" />
+
+ <Button
+ android:id="@+id/rotation_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/rotation_demo" />
+
+ <Button
+ android:id="@+id/home_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/home_demo" />
+
+ <Button
+ android:id="@+id/secure_window_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/secure_window_demo" />
+
+ <Button
+ android:id="@+id/permissions_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/permissions_demo" />
+
+ <Button
+ android:id="@+id/latency_demo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onDemoSelected"
+ android:text="@string/latency_demo" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/permissions_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/permissions_demo_activity.xml
new file mode 100644
index 0000000..58a13a6
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/permissions_demo_activity.xml
@@ -0,0 +1,23 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <Button
+ android:id="@+id/request_permissions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onRequestPermissions"
+ android:text="@string/request_permissions" />
+
+ <Button
+ android:id="@+id/revoke_permissions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onRevokePermissions"
+ android:text="@string/revoke_permissions" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/rotation_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/rotation_demo_activity.xml
new file mode 100644
index 0000000..2fd6aa9
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/rotation_demo_activity.xml
@@ -0,0 +1,31 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <TextView
+ android:id="@+id/current_orientation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="20dp"
+ android:text="@string/current_orientation"
+ android:textStyle="bold" />
+
+ <Button
+ android:id="@+id/portrait"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onChangeOrientation"
+ android:text="@string/portrait" />
+
+ <Button
+ android:id="@+id/landscape"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onChangeOrientation"
+ android:text="@string/landscape" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/secure_window_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/secure_window_demo_activity.xml
new file mode 100644
index 0000000..bf17bcd
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/secure_window_demo_activity.xml
@@ -0,0 +1,16 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <TextView
+ android:id="@+id/secure_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="20dp"
+ android:text="@string/secure_text"
+ android:textStyle="bold" />
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/sensor_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/sensor_demo_activity.xml
new file mode 100644
index 0000000..560d38d
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/sensor_demo_activity.xml
@@ -0,0 +1,50 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:padding="10dp">
+
+ <TextView
+ android:id="@+id/current_device"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:padding="8dp"
+ android:text="@string/current_device"
+ android:textStyle="bold" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:onClick="onChangeDevice"
+ android:text="@string/change_device" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <View
+ android:id="@+id/beam"
+ android:layout_width="match_parent"
+ android:layout_height="10dp"
+ android:background="#880808" />
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/layout/vibration_demo_activity.xml b/samples/VirtualDeviceManager/demos/res/layout/vibration_demo_activity.xml
new file mode 100644
index 0000000..eca776b
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/layout/vibration_demo_activity.xml
@@ -0,0 +1,65 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:padding="10dp">
+
+ <TextView
+ android:id="@+id/current_device"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:padding="8dp"
+ android:text="@string/current_device"
+ android:textStyle="bold" />
+
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:onClick="onChangeDevice"
+ android:text="@string/change_device" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="10dp">
+
+ <Button
+ android:id="@+id/vibrate"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onVibrate"
+ android:text="@string/vibrate" />
+
+ <Button
+ android:id="@+id/perform_haptic_feedback"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onPerformHapticFeedback"
+ android:text="@string/perform_haptic_feedback" />
+
+ <Button
+ android:id="@+id/ringtone_vibration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="onRingtoneVibration"
+ android:text="@string/ringtone_vibration" />
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/res/values/strings.xml b/samples/VirtualDeviceManager/demos/res/values/strings.xml
new file mode 100644
index 0000000..ee60b66
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/res/values/strings.xml
@@ -0,0 +1,29 @@
+<resources>
+ <string name="app_name" translatable="false">VDM Demos</string>
+ <string name="sensor_demo" translatable="false">VDM Sensor Demo</string>
+ <string name="vibration_demo" translatable="false">VDM Vibration Demo</string>
+ <string name="rotation_demo" translatable="false">VDM Rotation Demo</string>
+ <string name="home_demo" translatable="false">VDM Home Demo</string>
+ <string name="secure_window_demo" translatable="false">VDM Secure Window Demo</string>
+ <string name="permissions_demo" translatable="false">VDM Permissions Demo</string>
+ <string name="latency_demo" translatable="false">VDM Latency Demo</string>
+
+ <string name="current_device" translatable="false">Device: %s</string>
+ <string name="change_device" translatable="false">Change</string>
+
+ <string name="current_orientation" translatable="false">Orientation: %s</string>
+ <string name="portrait" translatable="false">Portrait</string>
+ <string name="landscape" translatable="false">Landscape</string>
+
+ <string name="send_home" translatable="false">Send HOME intent</string>
+ <string name="send_secondary_home" translatable="false">Send SECONDARY_HOME intent</string>
+
+ <string name="secure_text" translatable="false">You shall only be able to read this on the host device screen</string>
+
+ <string name="request_permissions" translatable="false">Request permissions</string>
+ <string name="revoke_permissions" translatable="false">Revoke permissions</string>
+
+ <string name="vibrate" translatable="false">Vibrate</string>
+ <string name="perform_haptic_feedback" translatable="false">Perform haptic feedback</string>
+ <string name="ringtone_vibration" translatable="false">Ringtone vibration</string>
+</resources>
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java
new file mode 100644
index 0000000..ca1ea7c
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+/** A view rendering a simple counter that is incremented every time onDraw() is called. */
+public class CounterView extends View {
+
+ private static final String TAG = "CounterView";
+ private static final int TEXT_SIZE_SP = 100;
+ private long mCounter = 0;
+ private final Paint mTextPaint = new Paint();
+
+ public CounterView(Context context) {
+ super(context);
+ init();
+ }
+
+ public CounterView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public CounterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ public CounterView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ mTextPaint.setColor(Color.RED);
+ mTextPaint.setStyle(Style.FILL);
+ mTextPaint.setTextSize(computeScaledTextSizeInPixels());
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.drawText(String.valueOf(mCounter), 0, 200, mTextPaint);
+ Log.e(TAG, "Rendered counter: " + mCounter);
+ mCounter++;
+ }
+
+ private float computeScaledTextSizeInPixels() {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ TEXT_SIZE_SP,
+ getContext().getResources().getDisplayMetrics());
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/HomeDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/HomeDemoActivity.java
new file mode 100644
index 0000000..be982b8
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/HomeDemoActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.app.ActivityOptions;
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/** Demo activity for showcasing Virtual Devices with home experience. */
+public final class HomeDemoActivity extends AppCompatActivity {
+
+ private DisplayManager mDisplayManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.home_demo_activity);
+
+ mDisplayManager = getSystemService(DisplayManager.class);
+ }
+
+ /** Handle home intent request. */
+ public void onSendHomeIntent(View view) {
+ sendIntentToDisplay(Intent.CATEGORY_HOME);
+ }
+
+ /** Handle secondary home intent request. */
+ public void onSendSecondaryHomeIntent(View view) {
+ sendIntentToDisplay(Intent.CATEGORY_SECONDARY_HOME);
+ }
+
+ private void sendIntentToDisplay(String category) {
+ Display[] displays = mDisplayManager.getDisplays();
+
+ String[] displayNames = new String[displays.length + 1];
+ displayNames[0] = "No display";
+ for (int i = 0; i < displays.length; ++i) {
+ displayNames[i + 1] = displays[i].getName();
+ }
+
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(HomeDemoActivity.this);
+ alertDialogBuilder.setTitle("Choose display");
+ alertDialogBuilder.setItems(
+ displayNames,
+ (dialog, which) -> {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(category);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ActivityOptions options = ActivityOptions.makeBasic();
+ if (which > 0) {
+ options.setLaunchDisplayId(displays[which - 1].getDisplayId());
+ }
+ startActivity(intent, options.toBundle());
+ });
+ alertDialogBuilder.show();
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/LatencyDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/LatencyDemoActivity.java
new file mode 100644
index 0000000..74e6137
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/LatencyDemoActivity.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Demo activity for testing latency in streaming with the VDM demo application. Increments a
+ * counter every ~second.
+ */
+public final class LatencyDemoActivity extends AppCompatActivity {
+
+ private static final int DELAY_MS = 1000;
+
+ private final ScheduledExecutorService mExecutor = Executors.newScheduledThreadPool(1);
+ private ScheduledFuture<?> mScheduledFuture;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.latency_demo_activity);
+ View counter = requireViewById(R.id.counter);
+
+ mScheduledFuture =
+ mExecutor.scheduleAtFixedRate(
+ counter::invalidate, 0, DELAY_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ protected void onDestroy() {
+ mScheduledFuture.cancel(true);
+ mExecutor.shutdown();
+ super.onDestroy();
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/MainActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/MainActivity.java
new file mode 100644
index 0000000..a478a52
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/MainActivity.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/** Launcher activity for all VDM demos. */
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+ }
+
+ /** Handles demo launch request. */
+ public void onDemoSelected(View view) {
+ switch (view.getId()) {
+ case R.id.home_demo -> startActivity(new Intent(this, HomeDemoActivity.class));
+ case R.id.sensor_demo -> startActivity(new Intent(this, SensorDemoActivity.class));
+ case R.id.rotation_demo -> startActivity(new Intent(this, RotationDemoActivity.class));
+ case R.id.secure_window_demo -> startActivity(
+ new Intent(this, SecureWindowDemoActivity.class));
+ case R.id.permissions_demo -> startActivity(
+ new Intent(this, PermissionsDemoActivity.class));
+ case R.id.latency_demo -> startActivity(new Intent(this, LatencyDemoActivity.class));
+ case R.id.vibration_demo -> startActivity(
+ new Intent(this, VibrationDemoActivity.class));
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/PermissionsDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/PermissionsDemoActivity.java
new file mode 100644
index 0000000..94fe727
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/PermissionsDemoActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.Manifest;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.Arrays;
+
+/** Demo activity for showcasing Virtual Devices with permission requests. */
+public final class PermissionsDemoActivity extends AppCompatActivity {
+
+ private static final int REQUEST_CODE_PERMISSIONS = 1001;
+
+ private static final String[] PERMISSIONS = {
+ Manifest.permission.READ_CONTACTS,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.READ_CALENDAR,
+ Manifest.permission.READ_SMS,
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.READ_MEDIA_AUDIO,
+ Manifest.permission.READ_MEDIA_IMAGES,
+ Manifest.permission.RECORD_AUDIO,
+ Manifest.permission.CAMERA,
+ Manifest.permission.BODY_SENSORS,
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.permissions_demo_activity);
+ }
+
+ /** Handle permission request. */
+ public void onRequestPermissions(View view) {
+ requestPermissions(PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+ }
+
+ /** Handle permission revoke. */
+ public void onRevokePermissions(View view) {
+ revokeSelfPermissionsOnKill(Arrays.asList(PERMISSIONS));
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/RotationDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/RotationDemoActivity.java
new file mode 100644
index 0000000..07158cb
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/RotationDemoActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.content.pm.ActivityInfo;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/** Demo activity for display rotation with VDM. */
+public final class RotationDemoActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.rotation_demo_activity);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ((TextView) requireViewById(R.id.current_orientation))
+ .setText(getString(R.string.current_orientation, getOrientationString()));
+ }
+
+ /** Handle orientation change request. */
+ public void onChangeOrientation(View view) {
+ int orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ if (view.getId() == R.id.portrait) {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ } else if (view.getId() == R.id.landscape) {
+ orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ }
+ setRequestedOrientation(orientation);
+ }
+
+ private String getOrientationString() {
+ return switch (getRequestedOrientation()) {
+ case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED -> "unspecified";
+ case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT -> "portrait";
+ case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE -> "landscape";
+ default -> "unknown";
+ };
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SecureWindowDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SecureWindowDemoActivity.java
new file mode 100644
index 0000000..c821615
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SecureWindowDemoActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/** Demo activity for testing secure content with VDM. */
+public final class SecureWindowDemoActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.secure_window_demo_activity);
+ getWindow()
+ .setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE);
+ }
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java
new file mode 100644
index 0000000..aada6ac
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.app.AlertDialog;
+import android.companion.virtual.VirtualDevice;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A minimal activity switching between sensor data from the default device and the virtual devices.
+ */
+public final class SensorDemoActivity extends AppCompatActivity implements SensorEventListener {
+
+ private static final String DEVICE_NAME_UNKNOWN = "Unknown";
+ private static final String DEVICE_NAME_DEFAULT = "Default - " + Build.MODEL;
+
+ private VirtualDeviceManager mVirtualDeviceManager;
+ private SensorManager mSensorManager;
+ private View mBeam;
+ private Context mDeviceContext;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.sensor_demo_activity);
+
+ mBeam = requireViewById(R.id.beam);
+
+ mVirtualDeviceManager = getSystemService(VirtualDeviceManager.class);
+ mSensorManager = getSystemService(SensorManager.class);
+
+ registerDeviceIdChangeListener(getMainExecutor(), this::changeSensorDevice);
+
+ mDeviceContext = this;
+ changeSensorDevice(mDeviceContext.getDeviceId());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mDeviceContext.unregisterDeviceIdChangeListener(this::changeSensorDevice);
+ mSensorManager.unregisterListener(this);
+ }
+
+ private void updateCurrentDeviceTextView(Context context) {
+ String deviceName = DEVICE_NAME_UNKNOWN;
+ if (context.getDeviceId() == Context.DEVICE_ID_DEFAULT) {
+ deviceName = DEVICE_NAME_DEFAULT;
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ VirtualDevice device = mVirtualDeviceManager.getVirtualDevice(context.getDeviceId());
+ deviceName = Objects.requireNonNull(device).getName();
+ } else {
+ for (VirtualDevice virtualDevice : mVirtualDeviceManager.getVirtualDevices()) {
+ if (virtualDevice.getDeviceId() == context.getDeviceId()) {
+ deviceName = virtualDevice.getName();
+ break;
+ }
+ }
+ }
+ TextView currentDevice = requireViewById(R.id.current_device);
+ currentDevice.setText(context.getString(R.string.current_device, deviceName));
+ }
+
+ /** Handle device change request. */
+ public void onChangeDevice(View view) {
+ List<VirtualDevice> virtualDevices = mVirtualDeviceManager.getVirtualDevices();
+ String[] devices = new String[virtualDevices.size() + 1];
+ devices[0] = DEVICE_NAME_DEFAULT;
+ for (int i = 0; i < virtualDevices.size(); ++i) {
+ devices[i + 1] = virtualDevices.get(i).getName();
+ }
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
+ alertDialogBuilder.setTitle("Available devices");
+ alertDialogBuilder.setItems(
+ devices,
+ (dialog, which) -> {
+ int deviceId =
+ which > 0
+ ? virtualDevices.get(which - 1).getDeviceId()
+ : Context.DEVICE_ID_DEFAULT;
+ changeSensorDevice(deviceId);
+ });
+ alertDialogBuilder.show();
+ }
+
+ private void changeSensorDevice(int deviceId) {
+ mDeviceContext.unregisterDeviceIdChangeListener(this::changeSensorDevice);
+ mSensorManager.unregisterListener(this);
+
+ mDeviceContext = createDeviceContext(deviceId);
+
+ updateCurrentDeviceTextView(mDeviceContext);
+
+ mSensorManager = mDeviceContext.getSystemService(SensorManager.class);
+ Objects.requireNonNull(mSensorManager);
+ Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (sensor != null) {
+ mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI);
+ }
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ float x = event.values[0];
+ float z = event.values[2];
+ float magnitude = (float) Math.sqrt(x * x + z * z);
+ float angle = (float) (Math.signum(x) * Math.acos(z / magnitude));
+ float angleDegrees = (float) Math.toDegrees(angle);
+ mBeam.setRotation(angleDegrees);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java
new file mode 100644
index 0000000..49fe0d4
--- /dev/null
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.demos;
+
+import android.app.AlertDialog;
+import android.companion.virtual.VirtualDevice;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.List;
+import java.util.Objects;
+
+/** A minimal activity switching between vibration on the default device and the virtual devices. */
+public final class VibrationDemoActivity extends AppCompatActivity {
+
+ private static final String DEVICE_NAME_UNKNOWN = "Unknown";
+ private static final String DEVICE_NAME_DEFAULT = "Default - " + Build.MODEL;
+
+ private VirtualDeviceManager mVirtualDeviceManager;
+ private Vibrator mVibrator;
+ private Context mDeviceContext;
+
+ private Ringtone mRingtone;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.vibration_demo_activity);
+
+ mVirtualDeviceManager = getSystemService(VirtualDeviceManager.class);
+ mVibrator = getSystemService(Vibrator.class);
+
+ registerDeviceIdChangeListener(getMainExecutor(), this::changeVibratorDevice);
+
+ mDeviceContext = this;
+ changeVibratorDevice(mDeviceContext.getDeviceId());
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mDeviceContext.unregisterDeviceIdChangeListener(this::changeVibratorDevice);
+ }
+
+ private void updateCurrentDeviceTextView(Context context) {
+ String deviceName = DEVICE_NAME_UNKNOWN;
+ if (context.getDeviceId() == Context.DEVICE_ID_DEFAULT) {
+ deviceName = DEVICE_NAME_DEFAULT;
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ VirtualDevice device = mVirtualDeviceManager.getVirtualDevice(context.getDeviceId());
+ deviceName = Objects.requireNonNull(device).getName();
+ } else {
+ for (VirtualDevice virtualDevice : mVirtualDeviceManager.getVirtualDevices()) {
+ if (virtualDevice.getDeviceId() == context.getDeviceId()) {
+ deviceName = virtualDevice.getName();
+ break;
+ }
+ }
+ }
+ TextView currentDevice = requireViewById(R.id.current_device);
+ currentDevice.setText(context.getString(R.string.current_device, deviceName));
+ }
+
+ /** Handle device change request. */
+ public void onChangeDevice(View view) {
+ List<VirtualDevice> virtualDevices = mVirtualDeviceManager.getVirtualDevices();
+ String[] devices = new String[virtualDevices.size() + 1];
+ devices[0] = DEVICE_NAME_DEFAULT;
+ for (int i = 0; i < virtualDevices.size(); ++i) {
+ devices[i + 1] = virtualDevices.get(i).getName();
+ }
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
+ alertDialogBuilder.setTitle("Available devices");
+ alertDialogBuilder.setItems(
+ devices,
+ (dialog, which) -> {
+ int deviceId =
+ which > 0
+ ? virtualDevices.get(which - 1).getDeviceId()
+ : Context.DEVICE_ID_DEFAULT;
+ changeVibratorDevice(deviceId);
+ });
+ alertDialogBuilder.show();
+ }
+
+ private void changeVibratorDevice(int deviceId) {
+ mDeviceContext.unregisterDeviceIdChangeListener(this::changeVibratorDevice);
+ mDeviceContext = createDeviceContext(deviceId);
+
+ updateCurrentDeviceTextView(mDeviceContext);
+
+ mVibrator = mDeviceContext.getSystemService(Vibrator.class);
+
+ Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+ mRingtone = RingtoneManager.getRingtone(mDeviceContext, uri);
+ mRingtone.setAudioAttributes(
+ new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setHapticChannelsMuted(false)
+ .build());
+ mRingtone.setHapticGeneratorEnabled(true);
+ }
+
+ /** Handle vibration request. */
+ public void onVibrate(View view) {
+ mVibrator.vibrate(VibrationEffect.EFFECT_HEAVY_CLICK);
+ }
+
+ /** Handle haptic feedback request. */
+ public void onPerformHapticFeedback(View view) {
+ view.performHapticFeedback(HapticFeedbackConstants.CONFIRM);
+ }
+
+ /** Handle request for ringtone with haptics enabled. */
+ public void onRingtoneVibration(View view) {
+ if (mRingtone.isPlaying()) {
+ mRingtone.stop();
+ } else {
+ mRingtone.play();
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/AndroidManifest.xml b/samples/VirtualDeviceManager/host/AndroidManifest.xml
new file mode 100644
index 0000000..9edf07d
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/AndroidManifest.xml
@@ -0,0 +1,82 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.example.android.vdmdemo.host"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk
+ android:minSdkVersion="34"
+ android:targetSdkVersion="35" />
+
+ <uses-feature android:name="android.software.companion_device_setup" />
+
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+
+ <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
+ <uses-permission android:name="android.permission.QUERY_AUDIO_STATE" />
+
+ <uses-permission
+ android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED"
+ tools:ignore="ProtectedPermissions" />
+ <uses-permission
+ android:name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING"
+ tools:ignore="ProtectedPermissions" />
+ <uses-permission
+ android:name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING"
+ tools:ignore="ProtectedPermissions" />
+ <uses-permission
+ android:name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY"
+ tools:ignore="ProtectedPermissions" />
+ <uses-permission
+ android:name="android.permission.ADD_TRUSTED_DISPLAY"
+ tools:ignore="ProtectedPermissions" />
+
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent>
+ </queries>
+
+ <application
+ android:name=".VdmHostApplication"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".SettingsActivity"
+ android:exported="true"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
+ <activity
+ android:name=".CustomLauncherActivity"
+ android:exported="true"
+ android:launchMode="singleTop"
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" />
+
+ <service
+ android:name=".VdmService"
+ android:exported="false"
+ android:foregroundServiceType="connectedDevice" />
+ <service
+ android:name=".NotificationListener"
+ android:exported="false"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService" />
+ </intent-filter>
+ </service>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml b/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml
new file mode 100644
index 0000000..a2ac85a
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/com.example.android.vdmdemo.host.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<permissions>
+ <privapp-permissions package="com.example.android.vdmdemo.host">
+ <permission name="android.permission.MODIFY_AUDIO_ROUTING" />
+ <permission name="android.permission.QUERY_AUDIO_STATE" />
+ <permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
+ <permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING" />
+ <permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" />
+ </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/drawable/close.xml b/samples/VirtualDeviceManager/host/res/drawable/close.xml
new file mode 100644
index 0000000..d5d2297
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/close.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/drawable/connected.xml b/samples/VirtualDeviceManager/host/res/drawable/connected.xml
new file mode 100644
index 0000000..92485d2
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/connected.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M720,640L575,640Q540,531 463.5,448Q387,365 281,320L720,320L720,640ZM80,800L80,680Q130,680 165,715Q200,750 200,800L80,800ZM280,800Q280,717 221.5,658.5Q163,600 80,600L80,520Q197,520 278.5,601.5Q360,683 360,800L280,800ZM440,800Q440,725 411.5,659.5Q383,594 334.5,545.5Q286,497 220.5,468.5Q155,440 80,440L80,360Q171,360 251,394.5Q331,429 391,489Q451,549 485.5,629Q520,709 520,800L440,800ZM800,800L600,800Q600,780 598.5,760Q597,740 594,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,286Q140,283 120,281.5Q100,280 80,280L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/drawable/settings.xml b/samples/VirtualDeviceManager/host/res/drawable/settings.xml
new file mode 100644
index 0000000..08b86ca
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/drawable/settings.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorControlNormal"
+ android:viewportWidth="960"
+ android:viewportHeight="960">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z" />
+</vector>
diff --git a/samples/VirtualDeviceManager/host/res/layout/activity_main.xml b/samples/VirtualDeviceManager/host/res/layout/activity_main.xml
new file mode 100644
index 0000000..56e5788
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/activity_main.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/main_tool_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="4dp"
+ android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/connectivity_fragment_container"
+ android:name="com.example.android.vdmdemo.common.ConnectivityFragment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/create_home_display"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:onClick="onCreateHomeDisplay"
+ android:text="@string/create_home_display" />
+
+ <Button
+ android:id="@+id/create_mirror_display"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:onClick="onCreateMirrorDisplay"
+ android:text="@string/create_mirror_display" />
+ </LinearLayout>
+
+ <include layout="@layout/custom_launcher" />
+
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml b/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml
new file mode 100644
index 0000000..ab08c81
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/activity_settings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/main_tool_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:background="?attr/colorPrimary"
+ android:elevation="4dp"
+ android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
+ app:navigationIcon="?homeAsUpIndicator"
+ app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/settings_fragment_container"
+ android:name="com.example.android.vdmdemo.host.SettingsActivity$SettingsFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/layout/custom_launcher.xml b/samples/VirtualDeviceManager/host/res/layout/custom_launcher.xml
new file mode 100644
index 0000000..55d4466
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/custom_launcher.xml
@@ -0,0 +1,8 @@
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/app_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:columnWidth="80dp"
+ android:horizontalSpacing="8dp"
+ android:numColumns="auto_fit"
+ android:verticalSpacing="8dp" />
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/layout/launcher_grid_item.xml b/samples/VirtualDeviceManager/host/res/layout/launcher_grid_item.xml
new file mode 100644
index 0000000..25bca7a
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/layout/launcher_grid_item.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="5dp">
+
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/app_icon_description" />
+
+ <TextView
+ android:id="@+id/app_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/app_icon"
+ android:layout_centerHorizontal="true"
+ android:autoSizeMaxTextSize="15sp"
+ android:autoSizeTextType="uniform"
+ android:breakStrategy="simple"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:maxLines="2"
+ android:textStyle="bold" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/menu/options.xml b/samples/VirtualDeviceManager/host/res/menu/options.xml
new file mode 100644
index 0000000..499ab4d
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/menu/options.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- LINT.IfChange -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/settings"
+ android:icon="@drawable/settings"
+ android:title="@string/settings"
+ app:showAsAction="always" />
+</menu>
+<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) -->
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/values/arrays.xml b/samples/VirtualDeviceManager/host/res/values/arrays.xml
new file mode 100644
index 0000000..3a9fcd3
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/values/arrays.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string-array translatable="false" name="device_profile_labels">
+ <item>App streaming</item>
+ <item>Nearby device streaming</item>
+ </string-array>
+ <string-array translatable="false" name="device_profiles">
+ <item>@string/app_streaming</item>
+ <item>@string/nearby_device_streaming</item>
+ </string-array>
+
+ <string-array translatable="false" name="display_ime_policy_labels">
+ <item>Show IME on the remote display</item>
+ <item>Show IME on the default display</item>
+ <item>Do not show IME</item>
+ </string-array>
+ <!-- Values correspond to WindowManager#DisplayImePolicy enums. -->
+ <string-array translatable="false" name="display_ime_policies">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/values/strings.xml b/samples/VirtualDeviceManager/host/res/values/strings.xml
new file mode 100644
index 0000000..e2001ee
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/values/strings.xml
@@ -0,0 +1,26 @@
+<resources>
+ <string name="app_name" translatable="false">VDM Host</string>
+ <string name="launcher_name" translatable="false">VDM Launcher</string>
+ <string name="create_home_display" translatable="false">Create Home Display</string>
+ <string name="create_mirror_display" translatable="false">Create Mirror Display</string>
+ <string name="app_icon_description" translatable="false">Application Icon</string>
+ <string name="settings" translatable="false">Settings</string>
+
+ <string name="pref_device_profile" translatable="false">device_profile</string>
+ <string name="pref_enable_recents" translatable="false">enable_recents</string>
+ <string name="pref_enable_cross_device_clipboard" translatable="false">enable_cross_device_clipboard</string>
+ <string name="pref_enable_client_sensors" translatable="false">enable_client_sensors</string>
+ <string name="pref_enable_client_audio" translatable="false">enable_client_audio</string>
+ <string name="pref_enable_display_rotation" translatable="false">enable_display_rotation</string>
+ <string name="pref_always_unlocked_device" translatable="false">always_unlocked_device</string>
+ <string name="pref_show_pointer_icon" translatable="false">show_pointer_icon</string>
+ <string name="pref_enable_custom_home" translatable="false">enable_custom_home</string>
+ <string name="pref_display_ime_policy" translatable="false">display_ime_policy</string>
+ <string name="pref_record_encoder_output" translatable="false">record_encoder_output</string>
+
+ <string name="internal_pref_enable_home_displays" translatable="false">enable_home_displays</string>
+ <string name="internal_pref_enable_mirror_displays" translatable="false">enable_mirror_displays</string>
+
+ <string name="app_streaming" translatable="false">android.app.role.COMPANION_DEVICE_APP_STREAMING</string>
+ <string name="nearby_device_streaming" translatable="false">android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING</string>
+</resources>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/res/values/styles.xml b/samples/VirtualDeviceManager/host/res/values/styles.xml
new file mode 100644
index 0000000..69da242
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+ <style name="Theme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowShowWallpaper">true</item>
+ </style>
+</resources>
diff --git a/samples/VirtualDeviceManager/host/res/xml/preferences.xml b/samples/VirtualDeviceManager/host/res/xml/preferences.xml
new file mode 100644
index 0000000..9f7f7a8
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/res/xml/preferences.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- LINT.IfChange -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto" >
+
+ <PreferenceCategory
+ android:key="general"
+ android:title="General"
+ app:iconSpaceReserved="false">
+ <ListPreference
+ android:key="@string/pref_device_profile"
+ android:title="Device profile"
+ android:entries="@array/device_profile_labels"
+ android:entryValues="@array/device_profiles"
+ android:defaultValue="@string/app_streaming"
+ app:useSimpleSummaryProvider="true"
+ app:iconSpaceReserved="false" />
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_recents"
+ android:title="Include streamed app in recents"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false"/>
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_cross_device_clipboard"
+ android:title="Enable cross-device clipboard"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="client_capabilities"
+ android:title="Client capabilities"
+ app:iconSpaceReserved="false">
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_client_sensors"
+ android:title="Enable client sensors"
+ android:defaultValue="true"
+ app:iconSpaceReserved="false" />
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_client_audio"
+ android:title="Enable client audio"
+ android:defaultValue="true"
+ app:iconSpaceReserved="false" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="display"
+ android:title="Displays"
+ app:iconSpaceReserved="false">
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_display_rotation"
+ android:title="Enable display rotation"
+ android:summary="Rotate the remote display instead of letterboxing or pillarboxing"
+ android:defaultValue="true"
+ app:iconSpaceReserved="false" />
+ <SwitchPreferenceCompat
+ android:key="@string/pref_always_unlocked_device"
+ android:title="Always unlocked"
+ android:summary="Remote displays remain unlocked even when the host is locked"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false" />
+ <SwitchPreferenceCompat
+ android:key="@string/pref_show_pointer_icon"
+ android:title="Show pointer icon"
+ android:summary="Mouse pointer on remote displays is visible"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false" />
+ <SwitchPreferenceCompat
+ android:key="@string/pref_enable_custom_home"
+ android:title="Custom home"
+ android:summary="Use a custom home activity instead of the default one on home displays"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="ime"
+ android:title="Input Method"
+ app:iconSpaceReserved="false">
+ <ListPreference
+ android:key="@string/pref_display_ime_policy"
+ android:title="Display IME policy"
+ android:entries="@array/display_ime_policy_labels"
+ android:entryValues="@array/display_ime_policies"
+ android:defaultValue="0"
+ app:useSimpleSummaryProvider="true"
+ app:iconSpaceReserved="false" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="debug"
+ android:title="Debug"
+ app:iconSpaceReserved="false">
+ <!--
+ When enabled, the encoder output of the host will be stored in:
+ /sdcard/Download/vdmdemo_encoder_output_[displayId].h264
+
+ After pulling this file to your machine this can be played back with:
+ ffplay -f h264 vdmdemo_encoder_output_[displayId].h264
+ -->
+ <SwitchPreferenceCompat
+ android:key="@string/pref_record_encoder_output"
+ android:title="Record encoder output"
+ android:summary="Store the host's media encoder output to a local file"
+ android:defaultValue="false"
+ app:iconSpaceReserved="false" />
+ </PreferenceCategory>
+
+</PreferenceScreen>
+<!-- LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) -->
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioStreamer.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioStreamer.java
new file mode 100644
index 0000000..a6fcb08
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioStreamer.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.util.concurrent.Uninterruptibles.joinUninterruptibly;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioManager.AudioPlaybackCallback;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioRecord;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.AudioFrame;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.StartAudio;
+import com.example.android.vdmdemo.common.RemoteEventProto.StopAudio;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.ByteString;
+
+import dagger.hilt.android.qualifiers.ApplicationContext;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+final class AudioStreamer implements AutoCloseable {
+ private static final String TAG = AudioStreamer.class.getSimpleName();
+
+ private static final int SAMPLE_RATE = 44000;
+ private static final AudioFormat AUDIO_FORMAT =
+ new AudioFormat.Builder()
+ .setSampleRate(SAMPLE_RATE)
+ .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .build();
+
+ private static final ImmutableSet<Integer> STREAMING_PLAYER_STATES =
+ ImmutableSet.of(
+ AudioPlaybackConfiguration.PLAYER_STATE_IDLE,
+ AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
+
+ private final Context mContext;
+ private final RemoteIo mRemoteIo;
+ private final AudioManager mAudioManager;
+ private final int mPlaybackSessionId;
+
+ private final Object mLock = new Object();
+
+ private final HandlerThread mHandlerThread = new HandlerThread("PolicyUpdater");
+ private final Handler mHandler;
+
+ @GuardedBy("mLock")
+ private AudioPolicy mAudioPolicy;
+
+ @GuardedBy("mLock")
+ private AudioMix mSessionIdAudioMix;
+
+ @GuardedBy("mLock")
+ private AudioPolicy mUidAudioPolicy;
+
+ @GuardedBy("mLock")
+ private AudioMix mUidAudioMix;
+
+ @GuardedBy("mLock")
+ private StreamingThread mStreamingThread;
+
+ @GuardedBy("mLock")
+ private AudioDeviceInfo mRemoteSubmixDevice;
+
+ @GuardedBy("mLock")
+ private AudioRecord mGhostRecord;
+
+ private ImmutableSet<Integer> mReroutedUids = ImmutableSet.of();
+
+ private final AudioPlaybackCallback mAudioPlaybackCallback =
+ new AudioPlaybackCallback() {
+ @Override
+ public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
+ super.onPlaybackConfigChanged(configs);
+
+ synchronized (mLock) {
+ boolean shouldStream = configs.stream().anyMatch(
+ c -> STREAMING_PLAYER_STATES.contains(c.getPlayerState())
+ && (mReroutedUids.contains(c.getClientUid())
+ || c.getSessionId() == mPlaybackSessionId));
+ if (mAudioPolicy == null) {
+ Log.d(
+ TAG,
+ "There's no active audio policy, ignoring playback "
+ + "config callback");
+ return;
+ }
+
+ if (mSessionIdAudioMix != null && shouldStream
+ && mStreamingThread == null) {
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setStartAudio(StartAudio.newBuilder())
+ .build());
+ mStreamingThread =
+ new StreamingThread(
+ mAudioPolicy.createAudioRecordSink(mSessionIdAudioMix),
+ mRemoteIo);
+ mStreamingThread.start();
+ } else if (!shouldStream && mStreamingThread != null) {
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setStopAudio(StopAudio.newBuilder())
+ .build());
+ mStreamingThread.stopStreaming();
+ joinUninterruptibly(mStreamingThread);
+ mStreamingThread = null;
+ }
+ }
+ }
+ };
+
+ @Inject
+ AudioStreamer(@ApplicationContext Context context, RemoteIo remoteIo) {
+ mContext = context;
+ mRemoteIo = remoteIo;
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mPlaybackSessionId = mAudioManager.generateAudioSessionId();
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mAudioManager.registerAudioPlaybackCallback(mAudioPlaybackCallback, mHandler);
+ }
+
+ public void start() {
+ mHandler.post(this::registerAudioPolicy);
+ }
+
+ public int getPlaybackSessionId() {
+ return mPlaybackSessionId;
+ }
+
+ private void registerAudioPolicy() {
+ AudioMixingRule mixingRule =
+ new AudioMixingRule.Builder()
+ .addMixRule(AudioMixingRule.RULE_MATCH_AUDIO_SESSION_ID, mPlaybackSessionId)
+ .build();
+ AudioMix audioMix =
+ new AudioMix.Builder(mixingRule)
+ .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+ .setFormat(AUDIO_FORMAT)
+ .build();
+
+ synchronized (mLock) {
+ if (mAudioPolicy != null) {
+ Log.w(TAG, "AudioPolicy is already registered");
+ return;
+ }
+ mAudioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build();
+ int ret = mAudioManager.registerAudioPolicy(mAudioPolicy);
+ if (ret != AudioManager.SUCCESS) {
+ Log.e(TAG, "Failed to register audio policy, error code " + ret);
+ mAudioPolicy = null;
+ return;
+ }
+
+ // This is a hacky way to determine audio device associated with audio mix.
+ // once audio record for the policy is initialized, audio policy manager
+ // will create remote submix instance so we can compare devices before and after
+ // to determine which device corresponds to this particular mix.
+ // The ghost audio record needs to be kept alive, releasing it would cause
+ // destruction of remote submix instances and potential problems when updating the
+ // UID-based render policy pointing to the remote submix device.
+ List<AudioDeviceInfo> preexistingRemoteSubmixDevices = getRemoteSubmixDevices();
+ mGhostRecord = mAudioPolicy.createAudioRecordSink(audioMix);
+ mGhostRecord.startRecording();
+ mRemoteSubmixDevice = getNewRemoteSubmixAudioDevice(preexistingRemoteSubmixDevices);
+ mSessionIdAudioMix = audioMix;
+ if (!mReroutedUids.isEmpty()) {
+ registerUidPolicy(mReroutedUids);
+ }
+ }
+ }
+
+ private @Nullable AudioDeviceInfo getNewRemoteSubmixAudioDevice(
+ Collection<AudioDeviceInfo> preexistingDevices) {
+ Set<String> preexistingAddresses =
+ preexistingDevices.stream()
+ .map(AudioDeviceInfo::getAddress)
+ .collect(Collectors.toSet());
+
+ List<AudioDeviceInfo> newDevices =
+ getRemoteSubmixDevices().stream()
+ .filter(dev -> !preexistingAddresses.contains(dev.getAddress()))
+ .collect(Collectors.toList());
+
+ if (newDevices.size() > 1) {
+ Log.e(TAG, "There's more than 1 new remote submix device");
+ return null;
+ }
+ if (newDevices.size() == 0) {
+ Log.e(TAG, "Didn't find new remote submix device");
+ return null;
+ }
+ return getOnlyElement(newDevices);
+ }
+
+ public void updateVdmUids(Set<Integer> uids) {
+ Log.w(TAG, "Updating mixing rule to reroute uids " + uids);
+ mHandler.post(
+ () -> {
+ synchronized (mLock) {
+ if (mRemoteSubmixDevice == null) {
+ Log.e(
+ TAG,
+ "Cannot update audio policy - remote submix device not known");
+ return;
+ }
+
+ if (mReroutedUids.equals(uids)) {
+ Log.d(TAG, "Not updating UID audio policy for same set of UIDs");
+ return;
+ }
+
+ updateAudioPolicies(uids);
+ }
+ });
+ }
+
+ // TODO(b/293611855) Use finer grained audio policy + mix controls once bugs are addressed
+ // This shouldn't unregister all audio policies just to re-register them again.
+ // That's inefficient and leads to audio leaks. But this is the most correct way to do this at
+ // this time.
+ @GuardedBy("mLock")
+ private void updateAudioPolicies(Set<Integer> uids) {
+ // TODO(b/293279299) Use Flagged API
+ // if (com.android.media.audio.flags.Flags.FLAG_AUDIO_POLICY_UPDATE_MIXING_RULES_API &&
+ // uidAudioMix != null && (!reroutedUids.isEmpty() && !uids.isEmpty())) {
+ // Pair<AudioMix, AudioMixingRule> update = Pair.create(uidAudioMix,
+ // createUidMixingRule(uids));
+ // uidAudioPolicy.updateMixingRules(Collections.singletonList(update));
+ // return;
+ // }
+
+ if (mAudioPolicy != null) {
+ mAudioManager.unregisterAudioPolicy(mAudioPolicy);
+ mAudioPolicy = null;
+ }
+ if (mUidAudioPolicy != null) {
+ mAudioManager.unregisterAudioPolicy(mUidAudioPolicy);
+ mUidAudioPolicy = null;
+ }
+
+ mReroutedUids = ImmutableSet.copyOf(uids);
+ registerAudioPolicy();
+ }
+
+ @GuardedBy("mLock")
+ private void registerUidPolicy(ImmutableSet<Integer> uids) {
+ mUidAudioMix =
+ new AudioMix.Builder(createUidMixingRule(uids))
+ .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
+ .setDevice(mRemoteSubmixDevice)
+ .setFormat(AUDIO_FORMAT)
+ .build();
+ AudioPolicy uidPolicy = new AudioPolicy.Builder(mContext).addMix(mUidAudioMix).build();
+ int ret = mAudioManager.registerAudioPolicy(uidPolicy);
+ if (ret != AudioManager.SUCCESS) {
+ Log.e(TAG, "Error " + ret + " while trying to register UID policy");
+ return;
+ }
+
+ mUidAudioPolicy = uidPolicy;
+ mReroutedUids = ImmutableSet.copyOf(uids);
+ }
+
+ public void stop() {
+ synchronized (mLock) {
+ if (mStreamingThread != null) {
+ mStreamingThread.stopStreaming();
+ joinUninterruptibly(mStreamingThread);
+ mStreamingThread = null;
+ }
+ if (mUidAudioPolicy != null) {
+ mAudioManager.unregisterAudioPolicy(mUidAudioPolicy);
+ mUidAudioPolicy = null;
+ }
+ if (mAudioPolicy != null) {
+ mAudioManager.unregisterAudioPolicy(mAudioPolicy);
+ mAudioPolicy = null;
+ }
+ if (mGhostRecord != null) {
+ mGhostRecord.stop();
+ mGhostRecord.release();
+ mGhostRecord = null;
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ mAudioManager.unregisterAudioPlaybackCallback(mAudioPlaybackCallback);
+ stop();
+ mHandlerThread.quitSafely();
+ }
+
+ private AudioMixingRule createUidMixingRule(Collection<Integer> uids) {
+ AudioMixingRule.Builder builder = new AudioMixingRule.Builder();
+ uids.forEach(uid -> builder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid));
+ return builder.build();
+ }
+
+ private List<AudioDeviceInfo> getRemoteSubmixDevices() {
+ return Arrays.stream(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+ .filter(AudioStreamer::deviceIsRemoteSubmixOut)
+ .collect(Collectors.toList());
+ }
+
+ private static boolean deviceIsRemoteSubmixOut(AudioDeviceInfo info) {
+ return info != null
+ && info.getType() == AudioDeviceInfo.TYPE_REMOTE_SUBMIX
+ && info.isSink();
+ }
+
+ private static class StreamingThread extends Thread {
+ private static final int BUFFER_SIZE =
+ AudioRecord.getMinBufferSize(
+ SAMPLE_RATE,
+ AudioFormat.CHANNEL_OUT_STEREO,
+ AudioFormat.ENCODING_PCM_16BIT);
+ private final RemoteIo mRemoteIo;
+ private final AudioRecord mAudioRecord;
+ private final AtomicBoolean mIsRunning = new AtomicBoolean(true);
+
+ StreamingThread(AudioRecord audioRecord, RemoteIo remoteIo) {
+ super();
+ mRemoteIo = Objects.requireNonNull(remoteIo);
+ mAudioRecord = Objects.requireNonNull(audioRecord);
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ Log.d(TAG, "Starting audio streaming");
+
+ if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
+ Log.e(TAG, "Audio record is not initialized");
+ return;
+ }
+
+ mAudioRecord.startRecording();
+ byte[] buffer = new byte[BUFFER_SIZE];
+ while (mIsRunning.get()) {
+ int ret = mAudioRecord.read(buffer, 0, buffer.length, AudioRecord.READ_BLOCKING);
+ if (ret <= 0) {
+ Log.e(TAG, "AudioRecord.read returned error code " + ret);
+ continue;
+ }
+
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setAudioFrame(
+ AudioFrame.newBuilder()
+ .setData(ByteString.copyFrom(buffer, 0, ret)))
+ .build());
+ }
+ Log.d(TAG, "Stopping audio streaming");
+ mAudioRecord.stop();
+ mAudioRecord.release();
+ }
+
+ void stopStreaming() {
+ mIsRunning.set(false);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java
new file mode 100644
index 0000000..b7f9030
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/CustomLauncherActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.app.WallpaperManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.GridView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+/** Simple activity that can act as a home/launcher on a virtual device. */
+@AndroidEntryPoint(AppCompatActivity.class)
+public class CustomLauncherActivity extends Hilt_CustomLauncherActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.custom_launcher);
+
+ WindowInsetsControllerCompat windowInsetsController =
+ WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
+ windowInsetsController.setSystemBarsBehavior(
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
+
+ GridView launcher = requireViewById(R.id.app_grid);
+ LauncherAdapter launcherAdapter =
+ new LauncherAdapter(getPackageManager(), WallpaperManager.getInstance(this));
+ launcher.setAdapter(launcherAdapter);
+ launcher.setOnItemClickListener(
+ (parent, v, position, id) -> {
+ Intent intent = launcherAdapter.createPendingRemoteIntent(position);
+ if (intent != null) {
+ startActivity(intent);
+ }
+ });
+ }
+
+ @Override
+ @SuppressWarnings("MissingSuperCall")
+ public void onBackPressed() {
+ // Do nothing
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java
new file mode 100644
index 0000000..bbe0c5f
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/DisplayRepository.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.view.Display;
+
+import androidx.annotation.GuardedBy;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+final class DisplayRepository {
+
+ @GuardedBy("mDisplayRepository")
+ private final List<RemoteDisplay> mDisplayRepository = new ArrayList<>();
+
+ @Inject
+ DisplayRepository() {}
+
+ void addDisplay(RemoteDisplay display) {
+ synchronized (mDisplayRepository) {
+ mDisplayRepository.add(display);
+ }
+ }
+
+ void removeDisplay(int displayId) {
+ getDisplay(displayId).ifPresent(this::closeDisplay);
+ }
+
+ void removeDisplayByRemoteId(int remoteDisplayId) {
+ getDisplayByRemoteId(remoteDisplayId).ifPresent(this::closeDisplay);
+ }
+
+ void onDisplayChanged(int displayId) {
+ getDisplay(displayId).ifPresent(RemoteDisplay::onDisplayChanged);
+ }
+
+ boolean resetDisplay(RemoteEvent event) {
+ Optional<RemoteDisplay> display = getDisplayByRemoteId(event.getDisplayId());
+ display.ifPresent(d -> d.reset(event.getDisplayCapabilities()));
+ return display.isPresent();
+ }
+
+ void clear() {
+ synchronized (mDisplayRepository) {
+ mDisplayRepository.forEach(RemoteDisplay::close);
+ mDisplayRepository.clear();
+ }
+ }
+
+ int[] getDisplayIds() {
+ synchronized (mDisplayRepository) {
+ return mDisplayRepository.stream()
+ .mapToInt(RemoteDisplay::getDisplayId)
+ .toArray();
+ }
+ }
+
+ int[] getRemoteDisplayIds() {
+ synchronized (mDisplayRepository) {
+ return mDisplayRepository.stream()
+ .mapToInt(RemoteDisplay::getRemoteDisplayId)
+ .toArray();
+ }
+ }
+
+ int getRemoteDisplayId(int displayId) {
+ return getDisplay(displayId)
+ .map(RemoteDisplay::getRemoteDisplayId)
+ .orElse(Display.INVALID_DISPLAY);
+ }
+
+ Optional<RemoteDisplay> getDisplayByIndex(int index) {
+ synchronized (mDisplayRepository) {
+ if (index < 0 || index >= mDisplayRepository.size()) {
+ return Optional.empty();
+ }
+ return Optional.of(mDisplayRepository.get(index));
+ }
+ }
+
+ private Optional<RemoteDisplay> getDisplay(int displayId) {
+ synchronized (mDisplayRepository) {
+ return mDisplayRepository.stream()
+ .filter(display -> display.getDisplayId() == displayId)
+ .findFirst();
+ }
+ }
+
+ private Optional<RemoteDisplay> getDisplayByRemoteId(int remoteDisplayId) {
+ synchronized (mDisplayRepository) {
+ return mDisplayRepository.stream()
+ .filter(display -> display.getRemoteDisplayId() == remoteDisplayId)
+ .findFirst();
+ }
+ }
+
+ private void closeDisplay(RemoteDisplay display) {
+ synchronized (mDisplayRepository) {
+ mDisplayRepository.remove(display);
+ }
+ display.close();
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/LauncherAdapter.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/LauncherAdapter.java
new file mode 100644
index 0000000..3b159d9
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/LauncherAdapter.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.app.WallpaperColors;
+import android.app.WallpaperManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.ResolveInfo;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.OvalShape;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class LauncherAdapter extends BaseAdapter {
+
+ private final List<ResolveInfo> mAvailableApps = new ArrayList<>();
+ private final PackageManager mPackageManager;
+ private int mTextColor = Color.BLACK;
+
+ LauncherAdapter(PackageManager packageManager) {
+ this(packageManager, null);
+ }
+
+ LauncherAdapter(PackageManager packageManager, WallpaperManager wallpaperManager) {
+ mPackageManager = packageManager;
+
+ if (wallpaperManager != null) {
+ WallpaperColors wallpaperColors =
+ wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ if ((wallpaperColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0) {
+ mTextColor = Color.WHITE;
+ }
+ }
+
+ mAvailableApps.addAll(
+ packageManager.queryIntentActivities(
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
+ ResolveInfoFlags.of(PackageManager.MATCH_ALL)));
+ }
+
+ @Override
+ public int getCount() {
+ return mAvailableApps.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mAvailableApps.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final ResolveInfo ri = mAvailableApps.get(position);
+ final Drawable img = ri.loadIcon(mPackageManager);
+ if (convertView == null) {
+ convertView =
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.launcher_grid_item, parent, false);
+ }
+ ImageView imageView = convertView.requireViewById(R.id.app_icon);
+ final Drawable background = new ShapeDrawable(new OvalShape());
+ imageView.setBackground(background);
+ imageView.setImageDrawable(img);
+
+ TextView textView = convertView.requireViewById(R.id.app_title);
+ textView.setText(ri.loadLabel(mPackageManager));
+ textView.setTextColor(mTextColor);
+ return convertView;
+ }
+
+ public Intent createPendingRemoteIntent(int position) {
+ if (position >= mAvailableApps.size()) {
+ return null;
+ }
+ ResolveInfo ri = mAvailableApps.get(position);
+ if (ri == null) {
+ return null;
+ }
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+ if (ri.activityInfo != null) {
+ intent.setComponent(
+ new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name));
+ } else {
+ intent.setComponent(new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name));
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
new file mode 100644
index 0000000..b397100
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.GridView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+import com.example.android.vdmdemo.common.ConnectionManager;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/**
+ * VDM Host activity, streaming apps to a remote device and processing the input coming from there.
+ */
+@AndroidEntryPoint(AppCompatActivity.class)
+public class MainActivity extends Hilt_MainActivity {
+ public static final String TAG = "VdmHost";
+
+ private VdmService mVdmService = null;
+ private GridView mLauncher = null;
+ private Button mHomeDisplayButton = null;
+ private Button mMirrorDisplayButton = null;
+
+ private final ServiceConnection mServiceConnection =
+ new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ Log.i(TAG, "Connected to VDM Service");
+ mVdmService = ((VdmService.LocalBinder) binder).getService();
+ mConnectionManager.startHostSession();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ Log.i(TAG, "Disconnected from VDM Service");
+ mVdmService = null;
+ }
+ };
+
+ private final ConnectionManager.ConnectionCallback mConnectionCallback =
+ new ConnectionManager.ConnectionCallback() {
+ @Override
+ public void onConnected(String remoteDeviceName) {
+ updateLauncherVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onDisconnected() {
+ updateLauncherVisibility(View.GONE);
+ mConnectionManager.startHostSession();
+ }
+ };
+
+ @Inject ConnectionManager mConnectionManager;
+ @Inject PreferenceController mPreferenceController;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = requireViewById(R.id.main_tool_bar);
+ setSupportActionBar(toolbar);
+
+ mHomeDisplayButton = requireViewById(R.id.create_home_display);
+ mHomeDisplayButton.setEnabled(
+ mPreferenceController.getBoolean(R.string.internal_pref_enable_home_displays));
+ mMirrorDisplayButton = requireViewById(R.id.create_mirror_display);
+ mMirrorDisplayButton.setEnabled(
+ mPreferenceController.getBoolean(R.string.internal_pref_enable_mirror_displays));
+
+ mLauncher = requireViewById(R.id.app_grid);
+ mLauncher.setVisibility(View.GONE);
+ LauncherAdapter launcherAdapter = new LauncherAdapter(getPackageManager());
+ mLauncher.setAdapter(launcherAdapter);
+ mLauncher.setOnItemClickListener(
+ (parent, v, position, id) -> {
+ Intent intent = launcherAdapter.createPendingRemoteIntent(position);
+ if (intent == null || mVdmService == null) {
+ return;
+ }
+ mVdmService.startStreaming(intent);
+ });
+ mLauncher.setOnItemLongClickListener(
+ (parent, v, position, id) -> {
+ Intent intent = launcherAdapter.createPendingRemoteIntent(position);
+ if (intent == null || mVdmService == null) {
+ return true;
+ }
+ int[] remoteDisplayIds = mVdmService.getRemoteDisplayIds();
+ if (remoteDisplayIds.length == 0) {
+ mVdmService.startStreaming(intent);
+ } else {
+ String[] displays = new String[remoteDisplayIds.length + 1];
+ for (int i = 0; i < remoteDisplayIds.length; ++i) {
+ displays[i] = "Display " + remoteDisplayIds[i];
+ }
+ displays[remoteDisplayIds.length] = "New display";
+ AlertDialog.Builder alertDialogBuilder =
+ new AlertDialog.Builder(MainActivity.this);
+ alertDialogBuilder.setTitle("Choose display");
+ alertDialogBuilder.setItems(
+ displays,
+ (dialog, which) -> {
+ if (which == remoteDisplayIds.length) {
+ mVdmService.startStreaming(intent);
+ } else {
+ mVdmService.startIntentOnDisplayIndex(intent, which);
+ }
+ });
+ alertDialogBuilder.show();
+ }
+ return true;
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ Intent intent = new Intent(this, VdmService.class);
+ startForegroundService(intent);
+ bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ ConnectionManager.ConnectionStatus connectionStatus =
+ mConnectionManager.getConnectionStatus();
+ updateLauncherVisibility(connectionStatus.connected ? View.VISIBLE : View.GONE);
+ mConnectionManager.addConnectionCallback(mConnectionCallback);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ unbindService(mServiceConnection);
+ mConnectionManager.removeConnectionCallback(mConnectionCallback);
+ }
+
+ private void updateLauncherVisibility(int visibility) {
+ runOnUiThread(
+ () -> {
+ if (mLauncher != null) {
+ mLauncher.setVisibility(visibility);
+ }
+ if (mHomeDisplayButton != null) {
+ mHomeDisplayButton.setVisibility(visibility);
+ }
+ if (mMirrorDisplayButton != null) {
+ mMirrorDisplayButton.setVisibility(visibility);
+ }
+ });
+ }
+
+ /** Process a home display request. */
+ public void onCreateHomeDisplay(View view) {
+ mVdmService.startStreamingHome();
+ }
+
+ /** Process a mirror display request. */
+ public void onCreateMirrorDisplay(View view) {
+ mVdmService.startMirroring();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/NotificationListener.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/NotificationListener.java
new file mode 100644
index 0000000..4e10a39
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/NotificationListener.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+/** NotificationListenerService to forward notifications shown on the host to the client. */
+public final class NotificationListener extends NotificationListenerService {
+
+ private static final String TAG = "VdmHost";
+
+ @Override
+ public void onNotificationRemoved(
+ StatusBarNotification notification,
+ NotificationListenerService.RankingMap rankingMap,
+ int reason) {
+
+ if (reason == NotificationListenerService.REASON_LOCKDOWN) {
+ Intent lockdownIntent = new Intent(this, VdmService.class);
+ lockdownIntent.setAction(VdmService.ACTION_LOCKDOWN);
+ PendingIntent pendingIntentLockdown =
+ PendingIntent.getService(this, 0, lockdownIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ try {
+ Log.i(
+ TAG,
+ "Notification removed due to lockdown. Sending lockdown "
+ + "Intent to VdmService");
+ pendingIntentLockdown.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Error sending lockdown Intent", e);
+ }
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java
new file mode 100644
index 0000000..13194d3
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/PreferenceController.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM;
+
+import android.companion.AssociationRequest;
+import android.companion.virtual.flags.Flags;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.ArrayMap;
+
+import androidx.annotation.StringRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+
+import dagger.hilt.android.qualifiers.ApplicationContext;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Manages the VDM Demo Host application settings and feature switches.
+ *
+ * <p>Upon creation, it will automatically update the preference values based on the current SDK
+ * version and the relevant feature flags.</p>
+ */
+@Singleton
+final class PreferenceController {
+
+ // LINT.IfChange
+ private static final Set<PrefRule<?>> RULES = Set.of(
+
+ // Exposed in the settings page
+
+ new BoolRule(R.string.pref_enable_cross_device_clipboard,
+ VANILLA_ICE_CREAM, Flags::crossDeviceClipboard),
+
+ new BoolRule(R.string.pref_enable_client_sensors, UPSIDE_DOWN_CAKE),
+
+ new BoolRule(R.string.pref_enable_display_rotation,
+ VANILLA_ICE_CREAM, Flags::consistentDisplayFlags)
+ .withDefaultValue(true),
+
+ new BoolRule(R.string.pref_enable_custom_home, VANILLA_ICE_CREAM, Flags::vdmCustomHome),
+
+ new StringRule(R.string.pref_display_ime_policy, VANILLA_ICE_CREAM, Flags::vdmCustomIme)
+ .withDefaultValue(String.valueOf(0)),
+
+ // TODO(b/316098039): Evaluate the minSdk of the prefs below.
+ new StringRule(R.string.pref_device_profile, VANILLA_ICE_CREAM)
+ .withDefaultValue(AssociationRequest.DEVICE_PROFILE_APP_STREAMING),
+ new BoolRule(R.string.pref_enable_recents, VANILLA_ICE_CREAM),
+ new BoolRule(R.string.pref_enable_client_audio, VANILLA_ICE_CREAM),
+ new BoolRule(R.string.pref_always_unlocked_device, VANILLA_ICE_CREAM),
+ new BoolRule(R.string.pref_show_pointer_icon, VANILLA_ICE_CREAM),
+ new BoolRule(R.string.pref_record_encoder_output, VANILLA_ICE_CREAM),
+
+ // Internal-only switches not exposed in the settings page.
+ // All of these are booleans acting as switches, while the above ones may be any type.
+
+ // TODO(b/316098039): Use the SysDecor flag on <= VIC
+ new InternalBoolRule(R.string.internal_pref_enable_home_displays,
+ VANILLA_ICE_CREAM, Flags::vdmCustomHome),
+
+ new InternalBoolRule(R.string.internal_pref_enable_mirror_displays,
+ VANILLA_ICE_CREAM,
+ Flags::consistentDisplayFlags, Flags::interactiveScreenMirror)
+ );
+ // LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options)
+
+ private final ArrayMap<Object, Map<String, Consumer<Object>>> mObservers = new ArrayMap<>();
+ private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener =
+ this::onPreferencesChanged;
+
+ private final Context mContext;
+ private final SharedPreferences mSharedPreferences;
+
+ @Inject
+ PreferenceController(@ApplicationContext Context context) {
+ mContext = context;
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ RULES.forEach(r -> r.evaluate(mContext, editor));
+ editor.commit();
+
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener);
+ }
+
+ /**
+ * Adds an observer for preference changes.
+ *
+ * @param key an object used only for bookkeeping.
+ * @param preferenceObserver a map from resource ID corresponding to the preference string key
+ * to the function that should be executed when that preference changes.
+ */
+ void addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver) {
+ ArrayMap<String, Consumer<Object>> stringObserver = new ArrayMap<>();
+ for (int resId : preferenceObserver.keySet()) {
+ stringObserver.put(
+ Objects.requireNonNull(mContext.getString(resId)),
+ preferenceObserver.get(resId));
+ }
+ mObservers.put(key, stringObserver);
+ }
+
+ /** Removes a previously added preference observer for the given key. */
+ void removePreferenceObserver(Object key) {
+ mObservers.remove(key);
+ }
+
+ /**
+ * Disables any {@link androidx.preference.Preference}, which is not satisfied by the current
+ * SDK version or the relevant feature flags.
+ *
+ * <p>This doesn't change any of the preference values, only disables the relevant UI elements
+ * in the preference screen.</p>
+ */
+ void evaluate(PreferenceManager preferenceManager) {
+ RULES.forEach(r -> r.evaluate(mContext, preferenceManager));
+ }
+
+ boolean getBoolean(@StringRes int resId) {
+ return mSharedPreferences.getBoolean(mContext.getString(resId), false);
+ }
+
+ String getString(@StringRes int resId) {
+ return Objects.requireNonNull(
+ mSharedPreferences.getString(mContext.getString(resId), null));
+ }
+
+ int getInt(@StringRes int resId) {
+ return Integer.valueOf(getString(resId));
+ }
+
+ private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) {
+ Map<String, ?> currentPreferences = sharedPreferences.getAll();
+ for (Map<String, Consumer<Object>> observer : mObservers.values()) {
+ Consumer<Object> consumer = observer.get(key);
+ if (consumer != null) {
+ consumer.accept(currentPreferences.get(key));
+ }
+ }
+ }
+
+ private abstract static class PrefRule<T> {
+ final @StringRes int mKey;
+ final int mMinSdk;
+ final BooleanSupplier[] mRequiredFlags;
+
+ protected T mDefaultValue;
+
+ PrefRule(@StringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags) {
+ mKey = key;
+ mMinSdk = minSdk;
+ mRequiredFlags = requiredFlags;
+ mDefaultValue = defaultValue;
+ }
+
+ void evaluate(Context context, SharedPreferences.Editor editor) {
+ if (!isSatisfied()) {
+ reset(context, editor);
+ }
+ }
+
+ void evaluate(Context context, PreferenceManager preferenceManager) {
+ Preference preference = preferenceManager.findPreference(context.getString(mKey));
+ if (preference != null) {
+ boolean enabled = isSatisfied();
+ if (preference.isEnabled() != enabled) {
+ preference.setEnabled(enabled);
+ }
+ }
+ }
+
+ protected abstract void reset(Context context, SharedPreferences.Editor editor);
+
+ protected boolean isSatisfied() {
+ return mMinSdk >= SDK_INT
+ && Arrays.stream(mRequiredFlags).allMatch(BooleanSupplier::getAsBoolean);
+ }
+
+ PrefRule<T> withDefaultValue(T defaultValue) {
+ mDefaultValue = defaultValue;
+ return this;
+ }
+ }
+
+ private static class BoolRule extends PrefRule<Boolean> {
+ BoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
+ super(key, false, minSdk, requiredFlags);
+ }
+
+ @Override
+ protected void reset(Context context, SharedPreferences.Editor editor) {
+ editor.putBoolean(context.getString(mKey), mDefaultValue);
+ }
+ }
+
+ private static class InternalBoolRule extends BoolRule {
+ InternalBoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
+ super(key, minSdk, requiredFlags);
+ }
+
+ @Override
+ void evaluate(Context context, SharedPreferences.Editor editor) {
+ editor.putBoolean(context.getString(mKey), isSatisfied());
+ }
+ }
+
+ private static class StringRule extends PrefRule<String> {
+ StringRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) {
+ super(key, null, minSdk, requiredFlags);
+ }
+
+ @Override
+ protected void reset(Context context, SharedPreferences.Editor editor) {
+ editor.putString(context.getString(mKey), mDefaultValue);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
new file mode 100644
index 0000000..12fcdce
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteDisplay.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceManager.VirtualDevice;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.hardware.display.VirtualDisplayConfig;
+import android.hardware.input.VirtualDpad;
+import android.hardware.input.VirtualDpadConfig;
+import android.hardware.input.VirtualKeyEvent;
+import android.hardware.input.VirtualKeyboard;
+import android.hardware.input.VirtualKeyboardConfig;
+import android.hardware.input.VirtualMouse;
+import android.hardware.input.VirtualMouseButtonEvent;
+import android.hardware.input.VirtualMouseConfig;
+import android.hardware.input.VirtualMouseRelativeEvent;
+import android.hardware.input.VirtualMouseScrollEvent;
+import android.hardware.input.VirtualNavigationTouchpad;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
+import android.hardware.input.VirtualTouchEvent;
+import android.hardware.input.VirtualTouchscreen;
+import android.hardware.input.VirtualTouchscreenConfig;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.DisplayCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.DisplayRotation;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteInputEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteKeyEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteMotionEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.StopStreaming;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.example.android.vdmdemo.common.VideoManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+class RemoteDisplay implements AutoCloseable {
+
+ private static final String TAG = "VdmHost";
+
+ private static final int DISPLAY_FPS = 60;
+
+ private static final int DEFAULT_VIRTUAL_DISPLAY_FLAGS =
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
+ | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
+ | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+
+ static final int DISPLAY_TYPE_APP = 0;
+ static final int DISPLAY_TYPE_HOME = 1;
+ static final int DISPLAY_TYPE_MIRROR = 2;
+ @IntDef(value = {DISPLAY_TYPE_APP, DISPLAY_TYPE_HOME, DISPLAY_TYPE_MIRROR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DisplayType {}
+
+ private final Context mContext;
+ private final RemoteIo mRemoteIo;
+ private final PreferenceController mPreferenceController;
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+ private final VirtualDisplay mVirtualDisplay;
+ private final VirtualDpad mDpad;
+ private final int mRemoteDisplayId;
+ private final Executor mPendingIntentExecutor;
+ private final VirtualDevice mVirtualDevice;
+ private final @DisplayType int mDisplayType;
+ private final AtomicBoolean mClosed = new AtomicBoolean(false);
+ private int mRotation;
+ private int mWidth;
+ private int mHeight;
+ private int mDpi;
+
+ private VideoManager mVideoManager;
+ private VirtualTouchscreen mTouchscreen;
+ private VirtualMouse mMouse;
+ private VirtualNavigationTouchpad mNavigationTouchpad;
+ private VirtualKeyboard mKeyboard;
+
+ @SuppressLint("WrongConstant")
+ RemoteDisplay(
+ Context context,
+ RemoteEvent event,
+ VirtualDevice virtualDevice,
+ RemoteIo remoteIo,
+ @DisplayType int displayType,
+ PreferenceController preferenceController) {
+ mContext = context;
+ mRemoteIo = remoteIo;
+ mRemoteDisplayId = event.getDisplayId();
+ mVirtualDevice = virtualDevice;
+ mPendingIntentExecutor = context.getMainExecutor();
+ mDisplayType = displayType;
+ mPreferenceController = preferenceController;
+
+ setCapabilities(event.getDisplayCapabilities());
+
+ int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
+ if (mPreferenceController.getBoolean(R.string.pref_enable_display_rotation)) {
+ flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
+ }
+ if (mDisplayType == DISPLAY_TYPE_MIRROR) {
+ flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+ }
+ mVirtualDisplay =
+ virtualDevice.createVirtualDisplay(
+ new VirtualDisplayConfig.Builder(
+ "VirtualDisplay" + mRemoteDisplayId, mWidth, mHeight, mDpi)
+ .setFlags(flags)
+ .setHomeSupported(mDisplayType == DISPLAY_TYPE_HOME
+ || mDisplayType == DISPLAY_TYPE_MIRROR)
+ .build(),
+ /* executor= */ Runnable::run,
+ /* callback= */ null);
+
+ mVirtualDevice.setDisplayImePolicy(
+ getDisplayId(), mPreferenceController.getInt(R.string.pref_display_ime_policy));
+
+ mDpad =
+ virtualDevice.createVirtualDpad(
+ new VirtualDpadConfig.Builder()
+ .setAssociatedDisplayId(mVirtualDisplay.getDisplay().getDisplayId())
+ .setInputDeviceName("vdmdemo-dpad" + mRemoteDisplayId)
+ .build());
+
+ remoteIo.addMessageConsumer(mRemoteEventConsumer);
+
+ reset();
+ }
+
+ void reset(DisplayCapabilities capabilities) {
+ setCapabilities(capabilities);
+ mVirtualDisplay.resize(mWidth, mHeight, mDpi);
+ reset();
+ }
+
+ private void reset() {
+ if (mVideoManager != null) {
+ mVideoManager.stop();
+ }
+ mVideoManager = VideoManager.createEncoder(mRemoteDisplayId, mRemoteIo,
+ mPreferenceController.getBoolean(R.string.pref_record_encoder_output));
+ Surface surface = mVideoManager.createInputSurface(mWidth, mHeight, DISPLAY_FPS);
+ mVirtualDisplay.setSurface(surface);
+
+ mRotation = mVirtualDisplay.getDisplay().getRotation();
+
+ if (mTouchscreen != null) {
+ mTouchscreen.close();
+ }
+ mTouchscreen =
+ mVirtualDevice.createVirtualTouchscreen(
+ new VirtualTouchscreenConfig.Builder(mWidth, mHeight)
+ .setAssociatedDisplayId(mVirtualDisplay.getDisplay().getDisplayId())
+ .setInputDeviceName("vdmdemo-touchscreen" + mRemoteDisplayId)
+ .build());
+
+ mVideoManager.startEncoding();
+ }
+
+ private void setCapabilities(DisplayCapabilities capabilities) {
+ mWidth = capabilities.getViewportWidth();
+ mHeight = capabilities.getViewportHeight();
+ mDpi = capabilities.getDensityDpi();
+
+ // Video encoder needs round dimensions...
+ mHeight -= mHeight % 10;
+ mWidth -= mWidth % 10;
+ }
+
+ void launchIntent(PendingIntent intent) {
+ mVirtualDevice.launchPendingIntent(
+ mVirtualDisplay.getDisplay().getDisplayId(),
+ intent,
+ mPendingIntentExecutor,
+ (result) -> {
+ switch (result) {
+ case VirtualDeviceManager.LAUNCH_SUCCESS:
+ Log.i(TAG, "launchIntent: Launched app successfully on display "
+ + mVirtualDisplay.getDisplay().getDisplayId());
+ break;
+ case VirtualDeviceManager.LAUNCH_FAILURE_NO_ACTIVITY:
+ case VirtualDeviceManager.LAUNCH_FAILURE_PENDING_INTENT_CANCELED:
+ Log.w(TAG, "launchIntent: Launching app failed with reason: "
+ + result);
+ break;
+ default:
+ Log.w(TAG, "launchIntent: Unexpected result when launching app: "
+ + result);
+ }
+ });
+ }
+
+ int getRemoteDisplayId() {
+ return mRemoteDisplayId;
+ }
+
+ int getDisplayId() {
+ return mVirtualDisplay.getDisplay().getDisplayId();
+ }
+
+ void onDisplayChanged() {
+ if (mRotation != mVirtualDisplay.getDisplay().getRotation()) {
+ mRotation = mVirtualDisplay.getDisplay().getRotation();
+ int rotationDegrees = displayRotationToDegrees(mRotation);
+ Log.v(TAG, "Notify client for rotation event: " + rotationDegrees);
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(getRemoteDisplayId())
+ .setDisplayRotation(
+ DisplayRotation.newBuilder()
+ .setRotationDegrees(rotationDegrees))
+ .build());
+ }
+ }
+
+ @SuppressWarnings("PendingIntentMutability")
+ void processRemoteEvent(RemoteEvent event) {
+ if (event.getDisplayId() != mRemoteDisplayId) {
+ return;
+ }
+ if (event.hasHomeEvent()) {
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN);
+ homeIntent.addCategory(Intent.CATEGORY_HOME);
+ homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ int targetDisplayId =
+ mDisplayType == DISPLAY_TYPE_MIRROR ? Display.DEFAULT_DISPLAY : getDisplayId();
+ mContext.startActivity(
+ homeIntent,
+ ActivityOptions.makeBasic().setLaunchDisplayId(targetDisplayId).toBundle());
+ } else if (event.hasInputEvent()) {
+ processInputEvent(event.getInputEvent());
+ } else if (event.hasStopStreaming() && event.getStopStreaming().getPause()) {
+ if (mVideoManager != null) {
+ mVideoManager.stop();
+ mVideoManager = null;
+ }
+ }
+ }
+
+ private void processInputEvent(RemoteInputEvent inputEvent) {
+ switch (inputEvent.getDeviceType()) {
+ case DEVICE_TYPE_NONE:
+ Log.e(TAG, "Received no input device type");
+ break;
+ case DEVICE_TYPE_DPAD:
+ mDpad.sendKeyEvent(remoteEventToVirtualKeyEvent(inputEvent));
+ break;
+ case DEVICE_TYPE_NAVIGATION_TOUCHPAD:
+ if (mNavigationTouchpad == null) {
+ // Any arbitrarily big enough nav touchpad would work.
+ Point displaySize = new Point(5000, 5000);
+ mNavigationTouchpad =
+ mVirtualDevice.createVirtualNavigationTouchpad(
+ new VirtualNavigationTouchpadConfig.Builder(
+ displaySize.x, displaySize.y)
+ .setAssociatedDisplayId(getDisplayId())
+ .setInputDeviceName(
+ "vdmdemo-navtouchpad" + mRemoteDisplayId)
+ .build());
+ }
+ mNavigationTouchpad.sendTouchEvent(remoteEventToVirtualTouchEvent(inputEvent));
+ break;
+ case DEVICE_TYPE_MOUSE:
+ processMouseEvent(inputEvent);
+ break;
+ case DEVICE_TYPE_TOUCHSCREEN:
+ mTouchscreen.sendTouchEvent(remoteEventToVirtualTouchEvent(inputEvent));
+ break;
+ case DEVICE_TYPE_KEYBOARD:
+ if (mKeyboard == null) {
+ mKeyboard =
+ mVirtualDevice.createVirtualKeyboard(
+ new VirtualKeyboardConfig.Builder()
+ .setInputDeviceName(
+ "vdmdemo-keyboard" + mRemoteDisplayId)
+ .setAssociatedDisplayId(getDisplayId())
+ .build());
+ }
+ mKeyboard.sendKeyEvent(remoteEventToVirtualKeyEvent(inputEvent));
+ break;
+ default:
+ Log.e(
+ TAG,
+ "processInputEvent got an invalid input device type: "
+ + inputEvent.getDeviceType().getNumber());
+ break;
+ }
+ }
+
+ private void processMouseEvent(RemoteInputEvent inputEvent) {
+ if (mMouse == null) {
+ mMouse =
+ mVirtualDevice.createVirtualMouse(
+ new VirtualMouseConfig.Builder()
+ .setAssociatedDisplayId(getDisplayId())
+ .setInputDeviceName("vdmdemo-mouse" + mRemoteDisplayId)
+ .build());
+ }
+ if (inputEvent.hasMouseButtonEvent()) {
+ mMouse.sendButtonEvent(
+ new VirtualMouseButtonEvent.Builder()
+ .setButtonCode(inputEvent.getMouseButtonEvent().getKeyCode())
+ .setAction(inputEvent.getMouseButtonEvent().getAction())
+ .build());
+ } else if (inputEvent.hasMouseScrollEvent()) {
+ mMouse.sendScrollEvent(
+ new VirtualMouseScrollEvent.Builder()
+ .setXAxisMovement(inputEvent.getMouseScrollEvent().getX())
+ .setYAxisMovement(inputEvent.getMouseScrollEvent().getY())
+ .build());
+ } else if (inputEvent.hasMouseRelativeEvent()) {
+ PointF cursorPosition = mMouse.getCursorPosition();
+ mMouse.sendRelativeEvent(
+ new VirtualMouseRelativeEvent.Builder()
+ .setRelativeX(
+ inputEvent.getMouseRelativeEvent().getX() - cursorPosition.x)
+ .setRelativeY(
+ inputEvent.getMouseRelativeEvent().getY() - cursorPosition.y)
+ .build());
+ } else {
+ Log.e(TAG, "Received an invalid mouse event");
+ }
+ }
+
+ private static int getVirtualTouchEventAction(int action) {
+ return switch (action) {
+ case MotionEvent.ACTION_POINTER_DOWN -> VirtualTouchEvent.ACTION_DOWN;
+ case MotionEvent.ACTION_POINTER_UP -> VirtualTouchEvent.ACTION_UP;
+ default -> action;
+ };
+ }
+
+ private static int getVirtualTouchEventToolType(int action) {
+ return switch (action) {
+ case MotionEvent.ACTION_CANCEL -> VirtualTouchEvent.TOOL_TYPE_PALM;
+ default -> VirtualTouchEvent.TOOL_TYPE_FINGER;
+ };
+ }
+
+ // Surface rotation is in opposite direction to display rotation.
+ // See https://developer.android.com/reference/android/view/Display?hl=en#getRotation()
+ private static int displayRotationToDegrees(int displayRotation) {
+ return switch (displayRotation) {
+ case Surface.ROTATION_90 -> -90;
+ case Surface.ROTATION_180 -> 180;
+ case Surface.ROTATION_270 -> 90;
+ default -> 0;
+ };
+ }
+
+ private static VirtualKeyEvent remoteEventToVirtualKeyEvent(RemoteInputEvent event) {
+ RemoteKeyEvent keyEvent = event.getKeyEvent();
+ return new VirtualKeyEvent.Builder()
+ .setEventTimeNanos((long) (event.getTimestampMs() * 1e6))
+ .setKeyCode(keyEvent.getKeyCode())
+ .setAction(keyEvent.getAction())
+ .build();
+ }
+
+ private static VirtualTouchEvent remoteEventToVirtualTouchEvent(RemoteInputEvent event) {
+ RemoteMotionEvent motionEvent = event.getTouchEvent();
+ return new VirtualTouchEvent.Builder()
+ .setEventTimeNanos((long) (event.getTimestampMs() * 1e6))
+ .setPointerId(motionEvent.getPointerId())
+ .setAction(getVirtualTouchEventAction(motionEvent.getAction()))
+ .setPressure(motionEvent.getPressure() * 255f)
+ .setToolType(getVirtualTouchEventToolType(motionEvent.getAction()))
+ .setX(motionEvent.getX())
+ .setY(motionEvent.getY())
+ .build();
+ }
+
+ @Override
+ public void close() {
+ if (mClosed.getAndSet(true)) { // Prevent double closure.
+ return;
+ }
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(getRemoteDisplayId())
+ .setStopStreaming(StopStreaming.newBuilder().setPause(false))
+ .build());
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ mDpad.close();
+ mTouchscreen.close();
+ if (mMouse != null) {
+ mMouse.close();
+ }
+ if (mNavigationTouchpad != null) {
+ mNavigationTouchpad.close();
+ }
+ if (mKeyboard != null) {
+ mKeyboard.close();
+ }
+ mVirtualDisplay.release();
+ if (mVideoManager != null) {
+ mVideoManager.stop();
+ mVideoManager = null;
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteSensorManager.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteSensorManager.java
new file mode 100644
index 0000000..bf75b6d
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RemoteSensorManager.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.companion.virtual.sensor.VirtualSensor;
+import android.companion.virtual.sensor.VirtualSensorCallback;
+import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.os.SystemClock;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteSensorEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.SensorConfiguration;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.google.common.primitives.Floats;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+final class RemoteSensorManager implements AutoCloseable {
+
+ private final RemoteIo mRemoteIo;
+ private final SparseArray<VirtualSensor> mVirtualSensors = new SparseArray<>(); // Keyed by type
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+
+ private final VirtualSensorCallback mVirtualSensorCallback = new SensorCallback();
+
+ private class SensorCallback implements VirtualSensorCallback {
+ @Override
+ public void onConfigurationChanged(
+ VirtualSensor sensor,
+ boolean enabled,
+ @NonNull Duration samplingPeriod,
+ @NonNull Duration batchReportLatency) {
+ mRemoteIo.sendMessage(RemoteEvent.newBuilder()
+ .setSensorConfiguration(SensorConfiguration.newBuilder()
+ .setSensorType(sensor.getType())
+ .setEnabled(enabled)
+ .setSamplingPeriodUs(
+ (int) TimeUnit.MICROSECONDS.convert(samplingPeriod))
+ .setBatchReportingLatencyUs(
+ (int) TimeUnit.MICROSECONDS.convert(batchReportLatency)))
+ .build());
+ }
+ }
+
+ RemoteSensorManager(RemoteIo remoteIo) {
+ this.mRemoteIo = remoteIo;
+ remoteIo.addMessageConsumer(mRemoteEventConsumer);
+ }
+
+ @Override
+ public void close() {
+ mVirtualSensors.clear();
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ }
+
+ VirtualSensorCallback getVirtualSensorCallback() {
+ return mVirtualSensorCallback;
+ }
+
+ void setVirtualSensors(List<VirtualSensor> virtualSensorList) {
+ for (VirtualSensor virtualSensor : virtualSensorList) {
+ mVirtualSensors.put(virtualSensor.getType(), virtualSensor);
+ }
+ }
+
+ void processRemoteEvent(RemoteEvent remoteEvent) {
+ if (remoteEvent.hasSensorEvent()) {
+ RemoteSensorEvent sensorEvent = remoteEvent.getSensorEvent();
+ VirtualSensor sensor = mVirtualSensors.get(sensorEvent.getSensorType());
+ if (sensor != null) {
+ sensor.sendEvent(
+ new VirtualSensorEvent.Builder(Floats.toArray(sensorEvent.getValuesList()))
+ .setTimestampNanos(SystemClock.elapsedRealtimeNanos())
+ .build());
+ }
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RunningVdmUidsTracker.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RunningVdmUidsTracker.java
new file mode 100644
index 0000000..2050589
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/RunningVdmUidsTracker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.companion.virtual.VirtualDeviceManager.ActivityListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+final class RunningVdmUidsTracker implements ActivityListener {
+ private static final String TAG = RunningVdmUidsTracker.class.getSimpleName();
+
+ private final PackageManager mPackageManager;
+ private final AudioStreamer mAudioStreamer;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final HashMap<Integer, HashSet<Integer>> mDisplayIdToRunningUids = new HashMap<>();
+
+ @GuardedBy("mLock")
+ private Set<Integer> mRunningVdmUids = Collections.emptySet();
+
+ RunningVdmUidsTracker(@NonNull Context context, @NonNull AudioStreamer audioStreamer) {
+ mPackageManager = Objects.requireNonNull(context).getPackageManager();
+ mAudioStreamer = Objects.requireNonNull(audioStreamer);
+ }
+
+ @Override
+ public void onTopActivityChanged(int displayId, @NonNull ComponentName componentName) {
+
+ Optional<Integer> topActivityUid = getUidForComponent(componentName);
+ if (topActivityUid.isEmpty()) {
+ Log.w(TAG, "Cannot determine UID for top activity component " + componentName);
+ return;
+ }
+
+ final Set<Integer> updatedUids;
+ synchronized (mLock) {
+ HashSet<Integer> displayUidSet =
+ mDisplayIdToRunningUids.computeIfAbsent(displayId, k -> new HashSet<>());
+ displayUidSet.add(topActivityUid.get());
+ mRunningVdmUids =
+ mDisplayIdToRunningUids.values().stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ updatedUids = mRunningVdmUids;
+ }
+
+ mAudioStreamer.updateVdmUids(updatedUids);
+ }
+
+ @Override
+ public void onDisplayEmpty(int displayId) {
+ Set<Integer> uidsBefore;
+ Set<Integer> uidsAfter;
+ synchronized (mLock) {
+ uidsBefore = mRunningVdmUids;
+ mDisplayIdToRunningUids.remove(displayId);
+ mRunningVdmUids =
+ mDisplayIdToRunningUids.values().stream()
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ uidsAfter = mRunningVdmUids;
+ }
+
+ if (!uidsAfter.equals(uidsBefore)) {
+ mAudioStreamer.updateVdmUids(uidsAfter);
+ }
+ }
+
+ private Optional<Integer> getUidForComponent(@NonNull ComponentName topActivity) {
+ try {
+ return Optional.of(
+ mPackageManager.getPackageUid(topActivity.getPackageName(), /* flags= */ 0));
+ } catch (NameNotFoundException e) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
new file mode 100644
index 0000000..3cc1aaf
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.preference.PreferenceFragmentCompat;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import javax.inject.Inject;
+
+/** VDM Host Settings activity. */
+@AndroidEntryPoint(AppCompatActivity.class)
+public class SettingsActivity extends Hilt_SettingsActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_settings);
+ Toolbar toolbar = requireViewById(R.id.main_tool_bar);
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationOnClickListener(v -> finish());
+ setTitle(getTitle() + " " + getString(R.string.settings));
+ }
+
+ @AndroidEntryPoint(PreferenceFragmentCompat.class)
+ public static final class SettingsFragment extends Hilt_SettingsActivity_SettingsFragment {
+
+ @Inject PreferenceController mPreferenceController;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.preferences, rootKey);
+ mPreferenceController.evaluate(getPreferenceManager());
+ }
+ }
+}
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmHostApplication.java
similarity index 70%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmHostApplication.java
index dd03545..a576f58 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmHostApplication.java
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+package com.example.android.vdmdemo.host;
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+import android.app.Application;
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+import dagger.hilt.android.HiltAndroidApp;
+
+/** VDM Host application class. */
+@HiltAndroidApp(Application.class)
+public class VdmHostApplication extends Hilt_VdmHostApplication {}
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
new file mode 100644
index 0000000..622a2a2
--- /dev/null
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/VdmService.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2023 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.example.android.vdmdemo.host;
+
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.app.role.RoleManager;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceManager.ActivityListener;
+import android.companion.virtual.VirtualDeviceParams;
+import android.companion.virtual.sensor.VirtualSensorConfig;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.hardware.display.DisplayManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.Display;
+
+import androidx.annotation.NonNull;
+
+import com.example.android.vdmdemo.common.ConnectionManager;
+import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.DisplayChangeEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
+import com.example.android.vdmdemo.common.RemoteEventProto.SensorCapabilities;
+import com.example.android.vdmdemo.common.RemoteEventProto.StartStreaming;
+import com.example.android.vdmdemo.common.RemoteIo;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * VDM Host service, streaming apps to a remote device and processing the input coming from there.
+ */
+@AndroidEntryPoint(Service.class)
+public final class VdmService extends Hilt_VdmService {
+
+ public static final String TAG = "VdmHost";
+
+ private static final String CHANNEL_ID = "com.example.android.vdmdemo.host.VdmService";
+ private static final int NOTIFICATION_ID = 1;
+
+ private static final String ACTION_STOP = "com.example.android.vdmdemo.host.VdmService.STOP";
+
+ public static final String ACTION_LOCKDOWN =
+ "com.example.android.vdmdemo.host.VdmService.LOCKDOWN";
+
+ /** Provides an instance of this service to bound clients. */
+ public class LocalBinder extends Binder {
+ VdmService getService() {
+ return VdmService.this;
+ }
+ }
+
+ private final IBinder mBinder = new LocalBinder();
+
+ @Inject ConnectionManager mConnectionManager;
+ @Inject RemoteIo mRemoteIo;
+ @Inject AudioStreamer mAudioStreamer;
+ @Inject PreferenceController mPreferenceController;
+ @Inject DisplayRepository mDisplayRepository;
+
+ private RemoteSensorManager mRemoteSensorManager = null;
+
+ private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent;
+ private VirtualDeviceManager.VirtualDevice mVirtualDevice;
+ private DeviceCapabilities mDeviceCapabilities;
+ private Intent mPendingRemoteIntent = null;
+ private @RemoteDisplay.DisplayType int mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP;
+ private DisplayManager mDisplayManager;
+
+ private final DisplayManager.DisplayListener mDisplayListener =
+ new DisplayManager.DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ mDisplayRepository.onDisplayChanged(displayId);
+ }
+ };
+
+ private final ConnectionManager.ConnectionCallback mConnectionCallback =
+ new ConnectionManager.ConnectionCallback() {
+ @Override
+ public void onDisconnected() {
+ mDeviceCapabilities = null;
+ closeVirtualDevice();
+ }
+ };
+
+ public VdmService() {}
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && ACTION_STOP.equals(intent.getAction())) {
+ Log.i(TAG, "Stopping VDM Service.");
+ mConnectionManager.disconnect();
+ stopForeground(STOP_FOREGROUND_REMOVE);
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ if (intent != null && ACTION_LOCKDOWN.equals(intent.getAction())) {
+ lockdown();
+ return START_STICKY;
+ }
+
+ NotificationChannel notificationChannel =
+ new NotificationChannel(
+ CHANNEL_ID, "VDM Service Channel", NotificationManager.IMPORTANCE_LOW);
+ notificationChannel.enableVibration(false);
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ Objects.requireNonNull(notificationManager).createNotificationChannel(notificationChannel);
+
+ Intent openIntent = new Intent(this, MainActivity.class);
+ PendingIntent pendingIntentOpen =
+ PendingIntent.getActivity(this, 0, openIntent, PendingIntent.FLAG_IMMUTABLE);
+ Intent stopIntent = new Intent(this, VdmService.class);
+ stopIntent.setAction(ACTION_STOP);
+ PendingIntent pendingIntentStop =
+ PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ Notification notification =
+ new Notification.Builder(this, CHANNEL_ID)
+ .setSmallIcon(R.drawable.connected)
+ .setContentTitle("VDM Demo running")
+ .setContentText("Click to open")
+ .setContentIntent(pendingIntentOpen)
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.close, "Stop", pendingIntentStop)
+ .build())
+ .setOngoing(true)
+ .build();
+ startForeground(NOTIFICATION_ID, notification);
+
+ return START_STICKY;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mConnectionManager.addConnectionCallback(mConnectionCallback);
+
+ mDisplayManager = getSystemService(DisplayManager.class);
+ Objects.requireNonNull(mDisplayManager).registerDisplayListener(mDisplayListener, null);
+
+ mRemoteIo.addMessageConsumer(mRemoteEventConsumer);
+
+ if (mPreferenceController.getBoolean(R.string.pref_enable_client_audio)) {
+ mAudioStreamer.start();
+ }
+
+ mPreferenceController.addPreferenceObserver(this, Map.of(
+ R.string.pref_enable_recents,
+ b -> updateDevicePolicy(POLICY_TYPE_RECENTS, !(Boolean) b),
+
+ R.string.pref_enable_cross_device_clipboard,
+ b -> updateDevicePolicy(POLICY_TYPE_CLIPBOARD, (Boolean) b),
+
+ R.string.pref_show_pointer_icon,
+ b -> {
+ if (mVirtualDevice != null) mVirtualDevice.setShowPointerIcon((Boolean) b);
+ },
+
+ R.string.pref_enable_client_audio,
+ b -> {
+ if ((Boolean) b) mAudioStreamer.start(); else mAudioStreamer.stop();
+ },
+
+ R.string.pref_display_ime_policy,
+ s -> {
+ if (mVirtualDevice != null) {
+ int policy = Integer.valueOf((String) s);
+ Arrays.stream(mDisplayRepository.getDisplayIds()).forEach(
+ displayId -> mVirtualDevice.setDisplayImePolicy(displayId, policy));
+ }
+ },
+
+ R.string.pref_enable_client_sensors, v -> recreateVirtualDevice(),
+ R.string.pref_device_profile, v -> recreateVirtualDevice(),
+ R.string.pref_always_unlocked_device, v -> recreateVirtualDevice(),
+ R.string.pref_enable_custom_home, v -> recreateVirtualDevice()
+ ));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mPreferenceController.removePreferenceObserver(this);
+ mConnectionManager.removeConnectionCallback(mConnectionCallback);
+ closeVirtualDevice();
+ mRemoteIo.removeMessageConsumer(mRemoteEventConsumer);
+ mDisplayManager.unregisterDisplayListener(mDisplayListener);
+ mAudioStreamer.close();
+ }
+
+ private void processRemoteEvent(RemoteEvent event) {
+ if (event.hasDeviceCapabilities()) {
+ Log.i(TAG, "Host received device capabilities");
+ mDeviceCapabilities = event.getDeviceCapabilities();
+ associateAndCreateVirtualDevice();
+ } else if (event.hasDisplayCapabilities() && !mDisplayRepository.resetDisplay(event)) {
+ RemoteDisplay remoteDisplay =
+ new RemoteDisplay(
+ this,
+ event,
+ mVirtualDevice,
+ mRemoteIo,
+ mPendingDisplayType,
+ mPreferenceController);
+ mDisplayRepository.addDisplay(remoteDisplay);
+ mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_APP;
+ if (mPendingRemoteIntent != null) {
+ remoteDisplay.launchIntent(
+ PendingIntent.getActivity(
+ this, 0, mPendingRemoteIntent, PendingIntent.FLAG_IMMUTABLE));
+ }
+ } else if (event.hasStopStreaming() && !event.getStopStreaming().getPause()) {
+ mDisplayRepository.removeDisplayByRemoteId(event.getDisplayId());
+ }
+ }
+
+ private void associateAndCreateVirtualDevice() {
+ CompanionDeviceManager cdm =
+ Objects.requireNonNull(getSystemService(CompanionDeviceManager.class));
+ RoleManager rm = Objects.requireNonNull(getSystemService(RoleManager.class));
+ final String deviceProfile = mPreferenceController.getString(R.string.pref_device_profile);
+ for (AssociationInfo associationInfo : cdm.getMyAssociations()) {
+ // Flashing the device clears the role and the permissions, but not the CDM
+ // associations.
+ // TODO(b/290596625): Remove the workaround to clear the associations if the role is not
+ // held.
+ if (!rm.isRoleHeld(deviceProfile)) {
+ cdm.disassociate(associationInfo.getId());
+ } else if (Objects.equals(associationInfo.getPackageName(), getPackageName())
+ && associationInfo.getDisplayName() != null
+ && Objects.equals(
+ associationInfo.getDisplayName().toString(),
+ mDeviceCapabilities.getDeviceName())) {
+ createVirtualDevice(associationInfo);
+ return;
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ AssociationRequest.Builder associationRequest =
+ new AssociationRequest.Builder()
+ .setDeviceProfile(deviceProfile)
+ .setDisplayName(mDeviceCapabilities.getDeviceName())
+ .setSelfManaged(true);
+ cdm.associate(
+ associationRequest.build(),
+ new CompanionDeviceManager.Callback() {
+ @Override
+ public void onAssociationPending(@NonNull IntentSender intentSender) {
+ try {
+ startIntentSender(intentSender, null, 0, 0, 0);
+ } catch (SendIntentException e) {
+ Log.e(
+ TAG,
+ "onAssociationPending: Failed to send device selection intent",
+ e);
+ }
+ }
+
+ @Override
+ public void onAssociationCreated(@NonNull AssociationInfo associationInfo) {
+ Log.i(TAG, "onAssociationCreated: ID " + associationInfo.getId());
+ createVirtualDevice(associationInfo);
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ Log.e(TAG, "onFailure: RemoteDevice Association failed " + error);
+ }
+ },
+ null);
+ Log.i(TAG, "createCdmAssociation: Waiting for association to happen");
+ }
+
+ private void createVirtualDevice(AssociationInfo associationInfo) {
+ VirtualDeviceParams.Builder virtualDeviceBuilder =
+ new VirtualDeviceParams.Builder()
+ .setName("VirtualDevice - " + mDeviceCapabilities.getDeviceName())
+ .setDevicePolicy(POLICY_TYPE_AUDIO, DEVICE_POLICY_CUSTOM)
+ .setAudioPlaybackSessionId(mAudioStreamer.getPlaybackSessionId());
+
+ if (mPreferenceController.getBoolean(R.string.pref_always_unlocked_device)) {
+ virtualDeviceBuilder.setLockState(LOCK_STATE_ALWAYS_UNLOCKED);
+ }
+
+ if (mPreferenceController.getBoolean(R.string.pref_enable_custom_home)) {
+ virtualDeviceBuilder.setHomeComponent(
+ new ComponentName(this, CustomLauncherActivity.class));
+ }
+
+ if (!mPreferenceController.getBoolean(R.string.pref_enable_recents)) {
+ virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM);
+ }
+
+ if (mPreferenceController.getBoolean(R.string.pref_enable_cross_device_clipboard)) {
+ virtualDeviceBuilder.setDevicePolicy(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_CUSTOM);
+ }
+
+ if (mPreferenceController.getBoolean(R.string.pref_enable_client_sensors)) {
+ for (SensorCapabilities sensor : mDeviceCapabilities.getSensorCapabilitiesList()) {
+ virtualDeviceBuilder.addVirtualSensorConfig(
+ new VirtualSensorConfig.Builder(
+ sensor.getType(), "Remote-" + sensor.getName())
+ .setMinDelay(sensor.getMinDelayUs())
+ .setMaxDelay(sensor.getMaxDelayUs())
+ .setPower(sensor.getPower())
+ .setResolution(sensor.getResolution())
+ .setMaximumRange(sensor.getMaxRange())
+ .build());
+ }
+
+ if (mDeviceCapabilities.getSensorCapabilitiesCount() > 0) {
+ mRemoteSensorManager = new RemoteSensorManager(mRemoteIo);
+ virtualDeviceBuilder
+ .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM)
+ .setVirtualSensorCallback(
+ MoreExecutors.directExecutor(),
+ mRemoteSensorManager.getVirtualSensorCallback());
+ }
+ }
+
+ VirtualDeviceManager vdm =
+ Objects.requireNonNull(getSystemService(VirtualDeviceManager.class));
+ mVirtualDevice =
+ vdm.createVirtualDevice(associationInfo.getId(), virtualDeviceBuilder.build());
+ if (mRemoteSensorManager != null) {
+ mRemoteSensorManager.setVirtualSensors(mVirtualDevice.getVirtualSensorList());
+ }
+
+ mVirtualDevice.setShowPointerIcon(
+ mPreferenceController.getBoolean(R.string.pref_show_pointer_icon));
+
+ mVirtualDevice.addActivityListener(
+ MoreExecutors.directExecutor(),
+ new ActivityListener() {
+
+ @Override
+ public void onTopActivityChanged(
+ int displayId, @NonNull ComponentName componentName) {
+ int remoteDisplayId = mDisplayRepository.getRemoteDisplayId(displayId);
+ if (remoteDisplayId == Display.INVALID_DISPLAY) {
+ return;
+ }
+ String title = "";
+ try {
+ ActivityInfo activityInfo =
+ getPackageManager().getActivityInfo(componentName, 0);
+ title = activityInfo.loadLabel(getPackageManager()).toString();
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Failed to get activity label for " + componentName);
+ }
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setDisplayId(remoteDisplayId)
+ .setDisplayChangeEvent(
+ DisplayChangeEvent.newBuilder().setTitle(title))
+ .build());
+ }
+
+ @Override
+ public void onDisplayEmpty(int displayId) {
+ Log.i(TAG, "Display " + displayId + " is empty, removing");
+ mDisplayRepository.removeDisplay(displayId);
+ }
+ });
+ mVirtualDevice.addActivityListener(
+ MoreExecutors.directExecutor(),
+ new RunningVdmUidsTracker(getApplicationContext(), mAudioStreamer));
+
+ Log.i(TAG, "Created virtual device");
+ }
+
+ private void lockdown() {
+ Log.i(TAG, "Initiating Lockdown.");
+ mDisplayRepository.clear();
+ }
+
+ private synchronized void closeVirtualDevice() {
+ if (mRemoteSensorManager != null) {
+ mRemoteSensorManager.close();
+ mRemoteSensorManager = null;
+ }
+ if (mVirtualDevice != null) {
+ Log.i(TAG, "Closing virtual device");
+ mDisplayRepository.clear();
+ mVirtualDevice.close();
+ mVirtualDevice = null;
+ }
+ }
+
+ int[] getRemoteDisplayIds() {
+ return mDisplayRepository.getRemoteDisplayIds();
+ }
+
+ void startStreamingHome() {
+ mPendingRemoteIntent = null;
+ mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_HOME;
+ mRemoteIo.sendMessage(RemoteEvent.newBuilder()
+ .setStartStreaming(StartStreaming.newBuilder().setHomeEnabled(true)).build());
+ }
+
+ void startMirroring() {
+ mPendingRemoteIntent = null;
+ mPendingDisplayType = RemoteDisplay.DISPLAY_TYPE_MIRROR;
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder()
+ .setStartStreaming(StartStreaming.newBuilder().setHomeEnabled(true))
+ .build());
+ }
+
+ void startStreaming(Intent intent) {
+ mPendingRemoteIntent = intent;
+ mRemoteIo.sendMessage(
+ RemoteEvent.newBuilder().setStartStreaming(StartStreaming.newBuilder()).build());
+ }
+
+ void startIntentOnDisplayIndex(Intent intent, int displayIndex) {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+ mDisplayRepository
+ .getDisplayByIndex(displayIndex)
+ .ifPresent(d -> d.launchIntent(pendingIntent));
+ }
+
+ private void recreateVirtualDevice() {
+ if (mVirtualDevice != null) {
+ closeVirtualDevice();
+ if (mDeviceCapabilities != null) {
+ associateAndCreateVirtualDevice();
+ }
+ }
+ }
+
+ private void updateDevicePolicy(int policyType, boolean custom) {
+ if (mVirtualDevice != null) {
+ mVirtualDevice.setDevicePolicy(
+ policyType, custom ? DEVICE_POLICY_CUSTOM : DEVICE_POLICY_DEFAULT);
+ }
+ }
+}
diff --git a/samples/VirtualDeviceManager/setup.sh b/samples/VirtualDeviceManager/setup.sh
new file mode 100755
index 0000000..4dcf645
--- /dev/null
+++ b/samples/VirtualDeviceManager/setup.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+
+# Copyright (C) 2023 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.
+
+function die() {
+ echo "${@}" >&2
+ exit 1;
+}
+
+function run_cmd_or_die() {
+ "${@}" > /dev/null || die "Command failed: ${*}"
+}
+
+function select_device() {
+ while :; do
+ read -r -p "Select a device to install the ${1} app (0-${DEVICE_COUNT}): " INDEX
+ [[ "${INDEX}" =~ ^[0-9]+$ ]] && ((INDEX >= 0 && INDEX <= DEVICE_COUNT)) && return "${INDEX}"
+ done
+}
+
+function install_app() {
+ if ! adb -s "${1}" install -r -d -g "${2}" > /dev/null 2>&1; then
+ adb -s "${1}" uninstall "com.example.android.vdmdemo.${3}" > /dev/null 2>&1
+ run_cmd_or_die adb -s "${1}" install -r -d -g "${2}"
+ fi
+}
+
+[[ -f build/make/envsetup.sh ]] || die "Run this script from the root of the tree."
+
+DEVICE_COUNT=$(adb devices -l | tail -n +2 | head -n -1 | wc -l)
+((DEVICE_COUNT > 0)) || die "No devices found"
+
+DEVICE_SERIALS=( $(adb devices -l | tail -n +2 | head -n -1 | awk '{ print $1 }') )
+DEVICE_NAMES=( $(adb devices -l | tail -n +2 | head -n -1 | awk '{ print $4 }') )
+HOST_SERIAL=""
+CLIENT_SERIAL=""
+
+echo
+echo "Available devices:"
+for i in "${!DEVICE_SERIALS[@]}"; do
+ echo -e "${i}: ${DEVICE_SERIALS[${i}]}\t${DEVICE_NAMES[${i}]}"
+done
+echo "${DEVICE_COUNT}: Do not install this app"
+echo
+select_device "VDM Host"
+HOST_INDEX=$?
+select_device "VDM Client"
+CLIENT_INDEX=$?
+echo
+
+if ((HOST_INDEX == DEVICE_COUNT)); then
+ echo "Not installing host app."
+else
+ HOST_SERIAL=${DEVICE_SERIALS[HOST_INDEX]}
+ HOST_NAME="${HOST_SERIAL} ${DEVICE_NAMES[HOST_INDEX]}"
+ echo "Using ${HOST_NAME} as host device."
+fi
+if ((CLIENT_INDEX == DEVICE_COUNT)); then
+ echo "Not installing client app."
+else
+ CLIENT_SERIAL=${DEVICE_SERIALS[CLIENT_INDEX]}
+ CLIENT_NAME="${CLIENT_SERIAL} ${DEVICE_NAMES[CLIENT_INDEX]}"
+ echo "Using ${CLIENT_NAME} as client device."
+fi
+
+APKS_TO_BUILD=""
+[[ -n "${HOST_SERIAL}" ]] && APKS_TO_BUILD="${APKS_TO_BUILD} VdmHost VdmDemos"
+[[ -n "${CLIENT_SERIAL}" ]] && APKS_TO_BUILD="${APKS_TO_BUILD} VdmClient"
+[[ -n "${APKS_TO_BUILD}" ]] || exit 0
+echo
+echo "Building APKs:${APKS_TO_BUILD}..."
+echo
+
+source ./build/envsetup.sh || die "Failed to set up environment"
+[[ -n "${ANDROID_BUILD_TOP}" ]] || run_cmd_or_die tapas "${APKS_TO_BUILD}"
+UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m -j "${APKS_TO_BUILD}" || die "Build failed"
+
+if [[ -n "${CLIENT_SERIAL}" ]]; then
+ echo
+ echo "Installing VdmClient.apk to ${CLIENT_NAME}..."
+ install_app "${CLIENT_SERIAL}" "${OUT}/system/app/VdmClient/VdmClient.apk" client
+fi
+
+if [[ -n "${HOST_SERIAL}" ]]; then
+ echo
+ echo "Installing VdmDemos.apk to ${HOST_NAME}..."
+ install_app "${HOST_SERIAL}" "${OUT}/system/app/VdmDemos/VdmDemos.apk" demos
+ echo
+
+ readonly PERM_BASENAME=com.example.android.vdmdemo.host.xml
+ readonly PERM_SRC="${ANDROID_BUILD_TOP}/development/samples/VirtualDeviceManager/host/${PERM_BASENAME}"
+ readonly PERM_DST="/system/etc/permissions/${PERM_BASENAME}"
+ readonly HOST_APK_DIR=/system/priv-app/VdmHost
+
+ echo "Preparing ${HOST_NAME} for privileged VdmHost installation..."
+ if adb -s "${HOST_SERIAL}" shell ls "${HOST_APK_DIR}/VdmHost.apk" > /dev/null 2>&1 \
+ && adb -s "${HOST_SERIAL}" pull "${PERM_DST}" "/tmp/${PERM_BASENAME}" > /dev/null 2>&1 \
+ && cmp --silent "/tmp/${PERM_BASENAME}" "${PERM_SRC}" \
+ && (adb -s "${HOST_SERIAL}" uninstall com.example.android.vdmdemo.host > /dev/null 2>&1 || true) \
+ && adb -s "${HOST_SERIAL}" install -r -d -g "${OUT}/${HOST_APK_DIR}/VdmHost.apk" > /dev/null 2>&1; then
+ echo "A privileged installation already found, installed VdmHost.apk to ${HOST_NAME}"
+ else
+ run_cmd_or_die adb -s "${HOST_SERIAL}" root
+ run_cmd_or_die adb -s "${HOST_SERIAL}" remount -R
+ run_cmd_or_die adb -s "${HOST_SERIAL}" wait-for-device
+ sleep 3 # Even after wait-for-device returns, the device may not be ready so give it some time.
+ run_cmd_or_die adb -s "${HOST_SERIAL}" root
+ run_cmd_or_die adb -s "${HOST_SERIAL}" remount
+ echo "Installing VdmHost.apk as a privileged app to ${HOST_NAME}..."
+ run_cmd_or_die adb -s "${HOST_SERIAL}" shell mkdir -p "${HOST_APK_DIR}"
+ run_cmd_or_die adb -s "${HOST_SERIAL}" push "${OUT}/${HOST_APK_DIR}/VdmHost.apk" "${HOST_APK_DIR}"
+ echo 'Copying privileged permissions...'
+ run_cmd_or_die adb -s "${HOST_SERIAL}" push "${PERM_SRC}" "${PERM_DST}"
+ echo 'Rebooting device...'
+ run_cmd_or_die adb -s "${HOST_SERIAL}" reboot
+ run_cmd_or_die adb -s "${HOST_SERIAL}" wait-for-device
+ fi
+fi
+
+echo
+echo 'Success!'
+echo
diff --git a/tools/winscope/.eslintrc.js b/tools/winscope/.eslintrc.js
index 8edd759..e38cfad 100644
--- a/tools/winscope/.eslintrc.js
+++ b/tools/winscope/.eslintrc.js
@@ -30,6 +30,12 @@
jasmine: true,
protractor: true,
},
+ ignorePatterns: [
+ // Perfetto trace processor sources. Either auto-generated (we want to keep them untouched)
+ // or copied from external/perfetto (we want to touch them as little as possible to allow
+ // future upgrading, diffing, conflicts merging, ...)
+ 'src/trace_processor/',
+ ],
rules: {
'no-unused-vars': 'off', // not very robust rule
diff --git a/tools/winscope/.gitignore b/tools/winscope/.gitignore
index 74f32dd..7e97075 100644
--- a/tools/winscope/.gitignore
+++ b/tools/winscope/.gitignore
@@ -5,13 +5,14 @@
/tmp
/out-tsc
/bazel-out
+/kotlin_build
# Node
/node_modules
npm-debug.log
-# Kotlin transpiled code
-/kotlin_build
+# Dependencies build artifacts (flickerlib, perfetto's trace processor, ...)
+/deps_build
# IDEs and editors
.idea/
diff --git a/tools/winscope/.prettierignore b/tools/winscope/.prettierignore
new file mode 100644
index 0000000..d1ba0cd
--- /dev/null
+++ b/tools/winscope/.prettierignore
@@ -0,0 +1,6 @@
+
+# Perfetto trace processor sources. Either auto-generated (we want to keep them untouched)
+# or copied from external/perfetto (we want to touch them as little as possible to allow
+# future upgrading, diffing, conflicts merging, ...)
+src/trace_processor/
+
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/karma.config.ci.js
similarity index 71%
rename from tools/winscope/src/interfaces/trace_position_update_emitter.ts
rename to tools/winscope/karma.config.ci.js
index dd03545..e1b7474 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/karma.config.ci.js
@@ -14,10 +14,16 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+const configCommon = require('./karma.config.common');
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+const configCi = (config) => {
+ config.set({
+ singleRun: true,
+ browsers: ['ChromeHeadless'],
+ });
+};
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+module.exports = (config) => {
+ configCommon(config);
+ configCi(config);
+};
diff --git a/tools/winscope/karma.conf.js b/tools/winscope/karma.config.common.js
similarity index 66%
rename from tools/winscope/karma.conf.js
rename to tools/winscope/karma.config.common.js
index bd03ace..b288ab2 100644
--- a/tools/winscope/karma.conf.js
+++ b/tools/winscope/karma.config.common.js
@@ -21,12 +21,23 @@
config.set({
frameworks: ['jasmine', 'webpack'],
plugins: ['karma-webpack', 'karma-chrome-launcher', 'karma-jasmine', 'karma-sourcemap-loader'],
- files: [{pattern: 'src/main_component_test.ts', watched: false}],
+ files: [
+ {pattern: 'src/main_unit_test.ts', watched: false},
+ {pattern: 'src/test/fixtures/**/*', included: false, served: true},
+ {
+ pattern: 'deps_build/trace_processor/to_be_served/engine_bundle.js',
+ included: false,
+ served: true,
+ },
+ {
+ pattern: 'deps_build/trace_processor/to_be_served/trace_processor.wasm',
+ included: false,
+ served: true,
+ },
+ ],
preprocessors: {
- 'src/main_component_test.ts': ['webpack', 'sourcemap'],
+ 'src/main_unit_test.ts': ['webpack', 'sourcemap'],
},
- singleRun: true,
- browsers: ['ChromeHeadless'],
webpack: webpackConfig,
});
};
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/karma.config.dev.js
similarity index 71%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to tools/winscope/karma.config.dev.js
index dd03545..b9a280b 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/karma.config.dev.js
@@ -14,10 +14,16 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+const configCommon = require('./karma.config.common');
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+const configDev = (config) => {
+ config.set({
+ singleRun: false,
+ browsers: ['Chrome'],
+ });
+};
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+module.exports = (config) => {
+ configCommon(config);
+ configDev(config);
+};
diff --git a/tools/winscope/package-lock.json b/tools/winscope/package-lock.json
index e041160..6bdf193 100644
--- a/tools/winscope/package-lock.json
+++ b/tools/winscope/package-lock.json
@@ -27,8 +27,6 @@
"dateformat": "^5.0.3",
"gl-matrix": "^3.4.3",
"html-loader": "^3.1.0",
- "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
- "html-webpack-plugin": "^5.5.0",
"html2canvas": "^1.4.1",
"jsbn": "^1.1.0",
"jsbn-rsa": "^1.0.4",
@@ -40,7 +38,7 @@
"three": "^0.143.0",
"ts-loader": "^9.3.0",
"tslib": "^2.3.0",
- "typescript": "~4.7.2",
+ "typescript": "^4.8.0",
"webgl-utils": "^1.0.1",
"webgl-utils.js": "^1.1.0",
"webpack-cli": "^4.10.0",
@@ -53,6 +51,7 @@
"@ngxs/devtools-plugin": "^3.7.4",
"@types/chrome": "^0.0.204",
"@types/dateformat": "^5.0.0",
+ "@types/gtag.js": "^0.0.13",
"@types/jasmine": "~4.3.1",
"@types/jquery": "^3.5.14",
"@types/jsbn": "^1.2.30",
@@ -62,9 +61,12 @@
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"angular2-template-loader": "^0.6.2",
+ "copy-webpack-plugin": "^11.0.0",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
+ "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
+ "html-webpack-plugin": "^5.5.0",
"jasmine": "~4.3.0",
"jasmine-core": "~4.1.0",
"karma": "~6.3.0",
@@ -83,6 +85,15 @@
"webpack": "^5.74.0"
}
},
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/@adobe/css-tools": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz",
@@ -243,6 +254,22 @@
}
}
},
+ "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": {
+ "version": "14.2.10",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
+ "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.15.0 || >=16.10.0",
+ "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+ "yarn": ">= 1.13.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler-cli": "^14.0.0",
+ "typescript": ">=4.6.2 <4.9",
+ "webpack": "^5.54.0"
+ }
+ },
"node_modules/@angular-devkit/build-angular/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -964,9 +991,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.20.10",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz",
- "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
+ "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -1065,16 +1092,16 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
- "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz",
+ "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==",
"dev": true,
"dependencies": {
- "@babel/compat-data": "^7.20.5",
- "@babel/helper-validator-option": "^7.18.6",
- "browserslist": "^4.21.3",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.0"
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-validator-option": "^7.22.5",
+ "@nicolo-ribaudo/semver-v6": "^6.3.3",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1083,15 +1110,6 @@
"@babel/core": "^7.0.0"
}
},
- "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/@babel/helper-create-class-features-plugin": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.7.tgz",
@@ -1383,9 +1401,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
- "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
+ "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -3228,9 +3246,9 @@
}
},
"node_modules/@jridgewell/source-map": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
- "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -3294,22 +3312,6 @@
"rxjs": "^6.5.3 || ^7.5.0"
}
},
- "node_modules/@ngtools/webpack": {
- "version": "14.2.10",
- "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
- "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
- "dev": true,
- "engines": {
- "node": "^14.15.0 || >=16.10.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "peerDependencies": {
- "@angular/compiler-cli": "^14.0.0",
- "typescript": ">=4.6.2 <4.9",
- "webpack": "^5.54.0"
- }
- },
"node_modules/@ngxs/devtools-plugin": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/@ngxs/devtools-plugin/-/devtools-plugin-3.7.6.tgz",
@@ -3355,6 +3357,15 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
+ "node_modules/@nicolo-ribaudo/semver-v6": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
+ "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3768,6 +3779,12 @@
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
+ "node_modules/@types/gtag.js": {
+ "version": "0.0.13",
+ "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.13.tgz",
+ "integrity": "sha512-yOXFkfnt1DQr1v9B4ERulJOGnbdVqnPHV8NG4nkQhnu4qbrJecQ06DlaKmSjI3nzIwBj5U9/X61LY4sTc2KbaQ==",
+ "dev": true
+ },
"node_modules/@types/har-format": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@@ -3777,7 +3794,8 @@
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
- "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "dev": true
},
"node_modules/@types/http-proxy": {
"version": "1.17.9",
@@ -4137,6 +4155,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
"integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.1"
@@ -4145,22 +4164,26 @@
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
- "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
+ "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+ "dev": true
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
- "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
+ "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+ "dev": true
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
- "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
+ "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+ "dev": true
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
"integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.11.1",
"@webassemblyjs/helper-api-error": "1.11.1",
@@ -4170,12 +4193,14 @@
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
- "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
+ "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+ "dev": true
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
"integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -4187,6 +4212,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
"integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+ "dev": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -4195,6 +4221,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
"integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+ "dev": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -4202,12 +4229,14 @@
"node_modules/@webassemblyjs/utf8": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
- "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
+ "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+ "dev": true
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
"integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -4223,6 +4252,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
"integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.1",
@@ -4235,6 +4265,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
"integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -4246,6 +4277,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
"integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-api-error": "1.11.1",
@@ -4259,6 +4291,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
"integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+ "dev": true,
"dependencies": {
"@webassemblyjs/ast": "1.11.1",
"@xtuc/long": "4.2.2"
@@ -4344,9 +4377,9 @@
}
},
"node_modules/acorn": {
- "version": "8.8.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
- "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"bin": {
"acorn": "bin/acorn"
},
@@ -4355,9 +4388,9 @@
}
},
"node_modules/acorn-import-assertions": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
- "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+ "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
"peerDependencies": {
"acorn": "^8"
}
@@ -4385,12 +4418,12 @@
}
},
"node_modules/adm-zip": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
- "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
+ "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"dev": true,
"engines": {
- "node": ">=0.3.0"
+ "node": ">=6.0"
}
},
"node_modules/agent-base": {
@@ -4574,6 +4607,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -5028,7 +5062,8 @@
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
@@ -5060,9 +5095,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.21.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
- "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"funding": [
{
"type": "opencollective",
@@ -5071,13 +5106,17 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001400",
- "electron-to-chromium": "^1.4.251",
- "node-releases": "^2.0.6",
- "update-browserslist-db": "^1.0.9"
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
+ "node-releases": "^2.0.12",
+ "update-browserslist-db": "^1.0.11"
},
"bin": {
"browserslist": "cli.js"
@@ -5268,9 +5307,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001441",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
- "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg==",
+ "version": "1.0.30001513",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz",
+ "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww==",
"funding": [
{
"type": "opencollective",
@@ -5279,6 +5318,10 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
]
},
@@ -5730,9 +5773,9 @@
"dev": true
},
"node_modules/cookiejar": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
- "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/copy-anything": {
"version": "2.0.6",
@@ -6082,6 +6125,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dev": true,
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
@@ -6097,6 +6141,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "dev": true,
"engines": {
"node": ">= 6"
},
@@ -6693,6 +6738,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
"integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "dev": true,
"dependencies": {
"utila": "~0.4"
}
@@ -6713,6 +6759,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
@@ -6726,6 +6773,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -6737,6 +6785,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
"dependencies": {
"domelementtype": "^2.2.0"
},
@@ -6751,6 +6800,7 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
@@ -6800,9 +6850,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
- "version": "1.4.284",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
- "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
+ "version": "1.4.453",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz",
+ "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -6852,9 +6902,9 @@
}
},
"node_modules/engine.io": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
- "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz",
+ "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==",
"dev": true,
"dependencies": {
"@types/cookie": "^0.4.1",
@@ -6865,26 +6915,26 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
- "engine.io-parser": "~5.0.3",
- "ws": "~8.2.3"
+ "engine.io-parser": "~5.1.0",
+ "ws": "~8.11.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
- "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
+ "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
"dev": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
- "version": "5.12.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
- "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -6903,6 +6953,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
@@ -6963,7 +7014,8 @@
"node_modules/es-module-lexer": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
- "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
+ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
+ "dev": true
},
"node_modules/es6-promise": {
"version": "4.2.8",
@@ -7367,6 +7419,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -8999,6 +9052,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/html-webpack-inline-source-plugin/-/html-webpack-inline-source-plugin-1.0.0-beta.2.tgz",
"integrity": "sha512-ydsEKdp0tnbmnqRAH2WSSMXerCNYhjes5b79uvP2BU3p6cyk+6ucNMsw5b5xD1QxphgvBBA3QqVmdcpu8QLlRQ==",
+ "dev": true,
"dependencies": {
"escape-string-regexp": "^1.0.5",
"slash": "^1.0.0",
@@ -9009,6 +9063,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
"integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9017,6 +9072,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
"integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==",
+ "dev": true,
"dependencies": {
"@types/html-minifier-terser": "^6.0.0",
"html-minifier-terser": "^6.0.2",
@@ -9051,6 +9107,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
@@ -9066,9 +9123,9 @@
}
},
"node_modules/http-cache-semantics": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
- "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"node_modules/http-deceiver": {
@@ -10118,9 +10175,9 @@
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+ "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -11807,9 +11864,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
- "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
"node_modules/node-source-walk": {
"version": "4.3.0",
@@ -12105,6 +12162,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
"dependencies": {
"boolbase": "^1.0.0"
},
@@ -12207,17 +12265,17 @@
}
},
"node_modules/optionator": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
- "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
"dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "type-check": "^0.4.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -13529,6 +13587,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
"integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "dev": true,
"dependencies": {
"lodash": "^4.17.20",
"renderkid": "^3.0.0"
@@ -14293,6 +14352,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
"integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "dev": true,
"dependencies": {
"css-select": "^4.1.3",
"dom-converter": "^0.2.0",
@@ -15055,9 +15115,9 @@
"integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA=="
},
"node_modules/serialize-javascript": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
- "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -15241,32 +15301,36 @@
}
},
"node_modules/socket.io": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
- "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz",
+ "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==",
"dev": true,
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
+ "cors": "~2.8.5",
"debug": "~4.3.2",
- "engine.io": "~6.2.1",
- "socket.io-adapter": "~2.4.0",
- "socket.io-parser": "~4.2.1"
+ "engine.io": "~6.5.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
- "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
- "dev": true
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+ "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+ "dev": true,
+ "dependencies": {
+ "ws": "~8.11.0"
+ }
},
"node_modules/socket.io-parser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
- "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
@@ -15387,7 +15451,8 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
- "deprecated": "See https://github.com/lydell/source-map-url#deprecated"
+ "deprecated": "See https://github.com/lydell/source-map-url#deprecated",
+ "dev": true
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
@@ -15570,6 +15635,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -15868,15 +15934,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
- "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
+ "version": "5.3.9",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+ "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.14",
+ "@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
- "serialize-javascript": "^6.0.0",
- "terser": "^5.14.1"
+ "serialize-javascript": "^6.0.1",
+ "terser": "^5.16.8"
},
"engines": {
"node": ">= 10.13.0"
@@ -15923,6 +15989,11 @@
"ajv": "^6.9.1"
}
},
+ "node_modules/terser-webpack-plugin/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -15945,6 +16016,23 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/terser-webpack-plugin/node_modules/terser": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz",
+ "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -16204,9 +16292,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -16421,9 +16509,9 @@
"dev": true
},
"node_modules/typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16433,9 +16521,9 @@
}
},
"node_modules/ua-parser-js": {
- "version": "0.7.32",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
- "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
+ "version": "0.7.35",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
+ "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
"dev": true,
"funding": [
{
@@ -16539,9 +16627,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
- "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"funding": [
{
"type": "opencollective",
@@ -16550,6 +16638,10 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
@@ -16557,7 +16649,7 @@
"picocolors": "^1.0.0"
},
"bin": {
- "browserslist-lint": "cli.js"
+ "update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
@@ -16592,7 +16684,8 @@
"node_modules/utila": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
- "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "dev": true
},
"node_modules/utils-merge": {
"version": "1.0.1",
@@ -16727,12 +16820,12 @@
}
},
"node_modules/webdriver-manager": {
- "version": "12.1.8",
- "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz",
- "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==",
+ "version": "12.1.9",
+ "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
+ "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
"dev": true,
"dependencies": {
- "adm-zip": "^0.4.9",
+ "adm-zip": "^0.5.2",
"chalk": "^1.1.1",
"del": "^2.2.0",
"glob": "^7.0.3",
@@ -16886,21 +16979,21 @@
"integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA=="
},
"node_modules/webpack": {
- "version": "5.75.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
- "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
+ "version": "5.88.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
+ "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
- "@types/estree": "^0.0.51",
- "@webassemblyjs/ast": "1.11.1",
- "@webassemblyjs/wasm-edit": "1.11.1",
- "@webassemblyjs/wasm-parser": "1.11.1",
+ "@types/estree": "^1.0.0",
+ "@webassemblyjs/ast": "^1.11.5",
+ "@webassemblyjs/wasm-edit": "^1.11.5",
+ "@webassemblyjs/wasm-parser": "^1.11.5",
"acorn": "^8.7.1",
- "acorn-import-assertions": "^1.7.6",
+ "acorn-import-assertions": "^1.9.0",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.10.0",
- "es-module-lexer": "^0.9.0",
+ "enhanced-resolve": "^5.15.0",
+ "es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
@@ -16909,9 +17002,9 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^3.1.0",
+ "schema-utils": "^3.2.0",
"tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.1.3",
+ "terser-webpack-plugin": "^5.3.7",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
@@ -17101,27 +17194,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/webpack-dev-server/node_modules/ws": {
- "version": "8.11.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
- "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
- "dev": true,
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"node_modules/webpack-merge": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz",
@@ -17163,6 +17235,142 @@
}
}
},
+ "node_modules/webpack/node_modules/@types/estree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/ast": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+ "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+ "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+ "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+ "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+ "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/ieee754": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+ "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/leb128": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+ "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/utf8": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+ "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/helper-wasm-section": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-opt": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6",
+ "@webassemblyjs/wast-printer": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+ "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+ "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+ "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/webpack/node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+ "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
"node_modules/webpack/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -17186,15 +17394,20 @@
"ajv": "^6.9.1"
}
},
+ "node_modules/webpack/node_modules/es-module-lexer": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
+ "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA=="
+ },
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/webpack/node_modules/schema-utils": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
- "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
@@ -17331,9 +17544,9 @@
"dev": true
},
"node_modules/ws": {
- "version": "8.2.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
- "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"dev": true,
"engines": {
"node": ">=10.0.0"
@@ -17446,6 +17659,12 @@
}
},
"dependencies": {
+ "@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true
+ },
"@adobe/css-tools": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz",
@@ -17560,6 +17779,13 @@
"webpack-subresource-integrity": "5.1.0"
},
"dependencies": {
+ "@ngtools/webpack": {
+ "version": "14.2.10",
+ "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
+ "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
+ "dev": true,
+ "requires": {}
+ },
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -18024,9 +18250,9 @@
}
},
"@babel/compat-data": {
- "version": "7.20.10",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz",
- "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz",
+ "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==",
"dev": true
},
"@babel/core": {
@@ -18104,24 +18330,16 @@
}
},
"@babel/helper-compilation-targets": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz",
- "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz",
+ "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==",
"dev": true,
"requires": {
- "@babel/compat-data": "^7.20.5",
- "@babel/helper-validator-option": "^7.18.6",
- "browserslist": "^4.21.3",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.0"
- },
- "dependencies": {
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- }
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-validator-option": "^7.22.5",
+ "@nicolo-ribaudo/semver-v6": "^6.3.3",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1"
}
},
"@babel/helper-create-class-features-plugin": {
@@ -18346,9 +18564,9 @@
"dev": true
},
"@babel/helper-validator-option": {
- "version": "7.18.6",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
- "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
+ "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
"dev": true
},
"@babel/helper-wrap-function": {
@@ -19575,9 +19793,9 @@
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
},
"@jridgewell/source-map": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
- "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@@ -19631,13 +19849,6 @@
"tslib": "^2.0.0"
}
},
- "@ngtools/webpack": {
- "version": "14.2.10",
- "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.10.tgz",
- "integrity": "sha512-sLHapZLVub6mEz5b19tf1VfIV1w3tYfg7FNPLeni79aldxu1FbP1v2WmiFAnMzrswqyK0bhTtxrl+Z/CLKqyoQ==",
- "dev": true,
- "requires": {}
- },
"@ngxs/devtools-plugin": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/@ngxs/devtools-plugin/-/devtools-plugin-3.7.6.tgz",
@@ -19670,6 +19881,12 @@
}
}
},
+ "@nicolo-ribaudo/semver-v6": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz",
+ "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==",
+ "dev": true
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -20023,6 +20240,12 @@
"integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==",
"dev": true
},
+ "@types/gtag.js": {
+ "version": "0.0.13",
+ "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.13.tgz",
+ "integrity": "sha512-yOXFkfnt1DQr1v9B4ERulJOGnbdVqnPHV8NG4nkQhnu4qbrJecQ06DlaKmSjI3nzIwBj5U9/X61LY4sTc2KbaQ==",
+ "dev": true
+ },
"@types/har-format": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz",
@@ -20032,7 +20255,8 @@
"@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
- "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "dev": true
},
"@types/http-proxy": {
"version": "1.17.9",
@@ -20303,6 +20527,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
"integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
+ "dev": true,
"requires": {
"@webassemblyjs/helper-numbers": "1.11.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.1"
@@ -20311,22 +20536,26 @@
"@webassemblyjs/floating-point-hex-parser": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
- "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
+ "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==",
+ "dev": true
},
"@webassemblyjs/helper-api-error": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
- "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
+ "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==",
+ "dev": true
},
"@webassemblyjs/helper-buffer": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
- "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
+ "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==",
+ "dev": true
},
"@webassemblyjs/helper-numbers": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
"integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
+ "dev": true,
"requires": {
"@webassemblyjs/floating-point-hex-parser": "1.11.1",
"@webassemblyjs/helper-api-error": "1.11.1",
@@ -20336,12 +20565,14 @@
"@webassemblyjs/helper-wasm-bytecode": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
- "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
+ "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==",
+ "dev": true
},
"@webassemblyjs/helper-wasm-section": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
"integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -20353,6 +20584,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
"integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
+ "dev": true,
"requires": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -20361,6 +20593,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
"integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
+ "dev": true,
"requires": {
"@xtuc/long": "4.2.2"
}
@@ -20368,12 +20601,14 @@
"@webassemblyjs/utf8": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
- "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
+ "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==",
+ "dev": true
},
"@webassemblyjs/wasm-edit": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
"integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -20389,6 +20624,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
"integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.1",
@@ -20401,6 +20637,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
"integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-buffer": "1.11.1",
@@ -20412,6 +20649,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
"integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/helper-api-error": "1.11.1",
@@ -20425,6 +20663,7 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
"integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
+ "dev": true,
"requires": {
"@webassemblyjs/ast": "1.11.1",
"@xtuc/long": "4.2.2"
@@ -20494,14 +20733,14 @@
}
},
"acorn": {
- "version": "8.8.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
- "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA=="
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw=="
},
"acorn-import-assertions": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
- "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+ "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
"requires": {}
},
"acorn-jsx": {
@@ -20522,9 +20761,9 @@
}
},
"adm-zip": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz",
- "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==",
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
+ "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"dev": true
},
"agent-base": {
@@ -20660,7 +20899,8 @@
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
},
"ansi-styles": {
"version": "3.2.1",
@@ -21008,7 +21248,8 @@
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
- "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true
},
"brace-expansion": {
"version": "2.0.1",
@@ -21036,14 +21277,14 @@
}
},
"browserslist": {
- "version": "4.21.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
- "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"requires": {
- "caniuse-lite": "^1.0.30001400",
- "electron-to-chromium": "^1.4.251",
- "node-releases": "^2.0.6",
- "update-browserslist-db": "^1.0.9"
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
+ "node-releases": "^2.0.12",
+ "update-browserslist-db": "^1.0.11"
}
},
"browserstack": {
@@ -21191,9 +21432,9 @@
"dev": true
},
"caniuse-lite": {
- "version": "1.0.30001441",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz",
- "integrity": "sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg=="
+ "version": "1.0.30001513",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001513.tgz",
+ "integrity": "sha512-pnjGJo7SOOjAGytZZ203Em95MRM8Cr6jhCXNF/FAXTpCTRTECnqQWLpiTRqrFtdYcth8hf4WECUpkezuYsMVww=="
},
"caseless": {
"version": "0.12.0",
@@ -21571,9 +21812,9 @@
"dev": true
},
"cookiejar": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
- "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"copy-anything": {
"version": "2.0.6",
@@ -21817,6 +22058,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
"integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dev": true,
"requires": {
"boolbase": "^1.0.0",
"css-what": "^6.0.1",
@@ -21828,7 +22070,8 @@
"css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
- "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "dev": true
},
"cssdb": {
"version": "7.2.0",
@@ -22258,6 +22501,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
"integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "dev": true,
"requires": {
"utila": "~0.4"
}
@@ -22278,6 +22522,7 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
@@ -22287,12 +22532,14 @@
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
- "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true
},
"domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
"requires": {
"domelementtype": "^2.2.0"
}
@@ -22301,6 +22548,7 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
@@ -22349,9 +22597,9 @@
"dev": true
},
"electron-to-chromium": {
- "version": "1.4.284",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
- "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
+ "version": "1.4.453",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.453.tgz",
+ "integrity": "sha512-BU8UtQz6CB3T7RIGhId4BjmjJVXQDujb0+amGL8jpcluFJr6lwspBOvkUbnttfpZCm4zFMHmjrX1QrdPWBBMjQ=="
},
"emoji-regex": {
"version": "8.0.0",
@@ -22394,9 +22642,9 @@
}
},
"engine.io": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
- "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz",
+ "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==",
"dev": true,
"requires": {
"@types/cookie": "^0.4.1",
@@ -22407,20 +22655,20 @@
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
- "engine.io-parser": "~5.0.3",
- "ws": "~8.2.3"
+ "engine.io-parser": "~5.1.0",
+ "ws": "~8.11.0"
}
},
"engine.io-parser": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
- "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
+ "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
"dev": true
},
"enhanced-resolve": {
- "version": "5.12.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
- "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
"requires": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -22435,7 +22683,8 @@
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
- "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true
},
"env-paths": {
"version": "2.2.1",
@@ -22481,7 +22730,8 @@
"es-module-lexer": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
- "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
+ "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
+ "dev": true
},
"es6-promise": {
"version": "4.2.8",
@@ -22688,7 +22938,8 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true
},
"escodegen": {
"version": "2.0.0",
@@ -23919,6 +24170,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/html-webpack-inline-source-plugin/-/html-webpack-inline-source-plugin-1.0.0-beta.2.tgz",
"integrity": "sha512-ydsEKdp0tnbmnqRAH2WSSMXerCNYhjes5b79uvP2BU3p6cyk+6ucNMsw5b5xD1QxphgvBBA3QqVmdcpu8QLlRQ==",
+ "dev": true,
"requires": {
"escape-string-regexp": "^1.0.5",
"slash": "^1.0.0",
@@ -23928,7 +24180,8 @@
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
- "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg=="
+ "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+ "dev": true
}
}
},
@@ -23936,6 +24189,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
"integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==",
+ "dev": true,
"requires": {
"@types/html-minifier-terser": "^6.0.0",
"html-minifier-terser": "^6.0.2",
@@ -23957,6 +24211,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
@@ -23965,9 +24220,9 @@
}
},
"http-cache-semantics": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
- "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"http-deceiver": {
@@ -24754,9 +25009,9 @@
}
},
"semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+ "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"requires": {
"lru-cache": "^6.0.0"
}
@@ -26088,9 +26343,9 @@
"optional": true
},
"node-releases": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
- "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A=="
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
"node-source-walk": {
"version": "4.3.0",
@@ -26322,6 +26577,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
"requires": {
"boolbase": "^1.0.0"
}
@@ -26394,17 +26650,17 @@
}
},
"optionator": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
- "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
"requires": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "type-check": "^0.4.0"
}
},
"ora": {
@@ -27271,6 +27527,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
"integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "dev": true,
"requires": {
"lodash": "^4.17.20",
"renderkid": "^3.0.0"
@@ -27876,6 +28133,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
"integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "dev": true,
"requires": {
"css-select": "^4.1.3",
"dom-converter": "^0.2.0",
@@ -28441,9 +28699,9 @@
}
},
"serialize-javascript": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
- "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"requires": {
"randombytes": "^2.1.0"
}
@@ -28598,29 +28856,33 @@
}
},
"socket.io": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
- "integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz",
+ "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==",
"dev": true,
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
+ "cors": "~2.8.5",
"debug": "~4.3.2",
- "engine.io": "~6.2.1",
- "socket.io-adapter": "~2.4.0",
- "socket.io-parser": "~4.2.1"
+ "engine.io": "~6.5.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
}
},
"socket.io-adapter": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
- "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
- "dev": true
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+ "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+ "dev": true,
+ "requires": {
+ "ws": "~8.11.0"
+ }
},
"socket.io-parser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
- "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
@@ -28712,7 +28974,8 @@
"source-map-url": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
- "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
+ "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
+ "dev": true
},
"sourcemap-codec": {
"version": "1.4.8",
@@ -28870,6 +29133,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"requires": {
"ansi-regex": "^5.0.1"
}
@@ -29090,15 +29354,15 @@
}
},
"terser-webpack-plugin": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
- "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
+ "version": "5.3.9",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+ "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
"requires": {
- "@jridgewell/trace-mapping": "^0.3.14",
+ "@jridgewell/trace-mapping": "^0.3.17",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
- "serialize-javascript": "^6.0.0",
- "terser": "^5.14.1"
+ "serialize-javascript": "^6.0.1",
+ "terser": "^5.16.8"
},
"dependencies": {
"ajv": {
@@ -29118,6 +29382,11 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -29132,6 +29401,17 @@
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
+ },
+ "terser": {
+ "version": "5.18.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz",
+ "integrity": "sha512-Ah19JS86ypbJzTzvUCX7KOsEIhDaRONungA4aYBjEP3JZRf4ocuDzTg4QWZnPn9DEMiMYGJPiSOy7aykoCc70w==",
+ "requires": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ }
}
}
},
@@ -29335,9 +29615,9 @@
},
"dependencies": {
"json5": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -29506,14 +29786,14 @@
"dev": true
},
"typescript": {
- "version": "4.7.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
- "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
},
"ua-parser-js": {
- "version": "0.7.32",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz",
- "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==",
+ "version": "0.7.35",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
+ "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
"dev": true
},
"unfetch": {
@@ -29586,9 +29866,9 @@
"dev": true
},
"update-browserslist-db": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
- "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
@@ -29623,7 +29903,8 @@
"utila": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
- "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA=="
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "dev": true
},
"utils-merge": {
"version": "1.0.1",
@@ -29731,12 +30012,12 @@
}
},
"webdriver-manager": {
- "version": "12.1.8",
- "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz",
- "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==",
+ "version": "12.1.9",
+ "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
+ "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
"dev": true,
"requires": {
- "adm-zip": "^0.4.9",
+ "adm-zip": "^0.5.2",
"chalk": "^1.1.1",
"del": "^2.2.0",
"glob": "^7.0.3",
@@ -29856,21 +30137,21 @@
"integrity": "sha512-cmO2aPd6gR6bK/ttdk8ZIypJfZMOcTvsvXv/LxXZjAFu5TC6vXqFrZYudlPuKxVsA34Pc8Fysq2rCnflu+wuuA=="
},
"webpack": {
- "version": "5.75.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
- "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
+ "version": "5.88.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz",
+ "integrity": "sha512-FROX3TxQnC/ox4N+3xQoWZzvGXSuscxR32rbzjpXgEzWudJFEJBpdlkkob2ylrv5yzzufD1zph1OoFsLtm6stQ==",
"requires": {
"@types/eslint-scope": "^3.7.3",
- "@types/estree": "^0.0.51",
- "@webassemblyjs/ast": "1.11.1",
- "@webassemblyjs/wasm-edit": "1.11.1",
- "@webassemblyjs/wasm-parser": "1.11.1",
+ "@types/estree": "^1.0.0",
+ "@webassemblyjs/ast": "^1.11.5",
+ "@webassemblyjs/wasm-edit": "^1.11.5",
+ "@webassemblyjs/wasm-parser": "^1.11.5",
"acorn": "^8.7.1",
- "acorn-import-assertions": "^1.7.6",
+ "acorn-import-assertions": "^1.9.0",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.10.0",
- "es-module-lexer": "^0.9.0",
+ "enhanced-resolve": "^5.15.0",
+ "es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
@@ -29879,13 +30160,149 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^3.1.0",
+ "schema-utils": "^3.2.0",
"tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.1.3",
+ "terser-webpack-plugin": "^5.3.7",
"watchpack": "^2.4.0",
"webpack-sources": "^3.2.3"
},
"dependencies": {
+ "@types/estree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+ },
+ "@webassemblyjs/ast": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+ "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+ "requires": {
+ "@webassemblyjs/helper-numbers": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+ }
+ },
+ "@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+ "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
+ },
+ "@webassemblyjs/helper-api-error": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
+ },
+ "@webassemblyjs/helper-buffer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+ "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+ },
+ "@webassemblyjs/helper-numbers": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+ "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+ "requires": {
+ "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
+ },
+ "@webassemblyjs/helper-wasm-section": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+ "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6"
+ }
+ },
+ "@webassemblyjs/ieee754": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+ "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+ "requires": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "@webassemblyjs/leb128": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+ "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+ "requires": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "@webassemblyjs/utf8": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
+ },
+ "@webassemblyjs/wasm-edit": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+ "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/helper-wasm-section": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-opt": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6",
+ "@webassemblyjs/wast-printer": "1.11.6"
+ }
+ },
+ "@webassemblyjs/wasm-gen": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+ "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "@webassemblyjs/wasm-opt": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+ "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6"
+ }
+ },
+ "@webassemblyjs/wasm-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+ "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "@webassemblyjs/wast-printer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+ "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+ "requires": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -29903,15 +30320,20 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
+ "es-module-lexer": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
+ "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA=="
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"schema-utils": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
- "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"requires": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
@@ -30021,13 +30443,6 @@
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.0.0"
}
- },
- "ws": {
- "version": "8.11.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
- "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
- "dev": true,
- "requires": {}
}
}
},
@@ -30149,9 +30564,9 @@
"dev": true
},
"ws": {
- "version": "8.2.3",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
- "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"dev": true,
"requires": {}
},
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index ef27d79..453fa88 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -6,21 +6,22 @@
"format:fix": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx prettier --write",
"eslint:check": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx eslint --format=unix",
"eslint:fix": "(ls *.js .*.js && find src/ -regextype egrep -regex '^.*(\\.ts|\\.js)$') |xargs npx eslint --format=unix --fix",
- "tslint:check": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json",
- "tslint:fix": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --fix",
- "deps_graph:check_cycles": "count=$(npx madge --extensions ts,js src/ --circular 2>&1 | awk '/Found.*circular dependencies/ {print $3}'); test ${count:-0} -le 10",
+ "tslint:check": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --exclude 'src/trace_processor/*'",
+ "tslint:fix": "find src/ -regextype egrep -regex '^.*\\.ts$' |xargs npx tslint -c google.tslint.json --exclude 'src/trace_processor/*' --fix",
+ "deps_graph:check_cycles": "npx madge --extensions ts,js src/ --circular",
"start": "webpack serve --config webpack.config.dev.js --open --hot --port 8080",
"start:remote_tool_mock": "webpack serve --config src/test/remote_tool_mock/webpack.config.js --open --hot --port 8081",
- "build:kotlin_legacy": "rm -rf kotlin_build && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -source-map -source-map-embed-sources always -module-kind commonjs -output kotlin_build/flicker.js ../../../platform_testing/libraries/flicker/src/android/tools/common",
- "build:kotlin": "rm -rf kotlin_build && mkdir kotlin_build && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xir-produce-js -Xir-only -Xir-module-name=flicker -Xtyped-arrays -source-map -source-map-embed-sources always -module-kind commonjs -target v8 -libraries ./node_modules/kotlin-compiler/lib/kotlin-stdlib-js.jar -output kotlin_build/flicker.js ../../../platform_testing/libraries/flicker/src/android/tools/common/",
- "build:prod": "webpack --config webpack.config.prod.js --progress",
- "build:remote_tool_mock": "webpack --config src/test/remote_tool_mock/webpack.config.js --progress",
- "build:all": "npm run build:kotlin && npm run build:prod && npm run build:remote_tool_mock",
- "test:unit": "webpack --config webpack.config.unit_test.js && jasmine dist/unit_test/bundle.js",
- "test:component": "npx karma start",
- "test:e2e": "rm -rf dist/e2e_test && npx tsc -p ./src/test/e2e && npx protractor protractor.config.js",
- "test:presubmit": "npm run test:unit && npm run test:component && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles",
- "test:all": "npm run test:unit && npm run test:component && npm run test:e2e && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles"
+ "build:kotlin_legacy": "rm -rf deps_build/flickerlib && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xuse-deprecated-legacy-compiler -source-map -source-map-embed-sources always -module-kind commonjs -output deps_build/flickerlib/flicker.js ../../../platform_testing/libraries/flicker/src/android/tools/common",
+ "build:kotlin": "rm -rf deps_build/flickerlib && mkdir -p deps_build/flickerlib && JAVA_OPTS='-Xmx2g -Xms1g' npx kotlinc-js -Xir-produce-js -Xir-only -Xir-module-name=flicker -Xtyped-arrays -source-map -source-map-embed-sources always -module-kind commonjs -target v8 -libraries ./node_modules/kotlin-compiler/lib/kotlin-stdlib-js.jar -output deps_build/flickerlib/flicker.js ../../../platform_testing/libraries/flicker/utils/src/android/tools/common/",
+ "build:trace_processor": "PERFETTO_TOP=../../../external/perfetto; (cd $PERFETTO_TOP && tools/install-build-deps --ui && ui/node ui/build.js --out trace_processor_build) && rm -rf deps_build/trace_processor && mkdir -p deps_build/trace_processor && rsync -ar $PERFETTO_TOP/trace_processor_build/ deps_build/trace_processor && mkdir deps_build/trace_processor/to_be_served && cp deps_build/trace_processor/ui/dist_version/engine_bundle.js deps_build/trace_processor/to_be_served/ && cp deps_build/trace_processor/wasm/trace_processor.wasm deps_build/trace_processor/to_be_served/",
+ "build:prod": "npm run build:trace_processor && npm run build:kotlin && webpack --config webpack.config.prod.js --progress && cp deps_build/trace_processor/to_be_served/* src/adb/winscope_proxy.py dist/prod/",
+ "install:chromedriver": "chrome_version=$(google-chrome --version |cut -d' ' -f3); rm -rf deps_build/chromedriver-linux64 && mkdir -p deps_build/chromedriver-linux64 && (cd deps_build/chromedriver-linux64 && wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${chrome_version}/linux64/chromedriver-linux64.zip && cd .. && unzip chromedriver-linux64/chromedriver-linux64.zip)",
+ "test:unit:ci": "npx karma start karma.config.ci.js",
+ "test:unit:dev": "npx karma start karma.config.dev.js",
+ "test:e2e": "npm run install:chromedriver && rm -rf dist/e2e_test && npx tsc -p ./src/test/e2e && npx protractor protractor.config.js",
+ "test:presubmit:quiet": "npm run test:unit:ci && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles",
+ "test:presubmit": "(npm run test:presubmit:quiet && printf '\\033[1m\\033[32mALL GREEN! \\U1F49A (Kean loves you)\\n') || (printf '\\033[1m\\033[31mFAILING! \\U1F92F (Kean is upset)\\n' && false)",
+ "test:all": "npm run test:unit:ci && npm run test:e2e && npm run format:check && npm run tslint:check && npm run eslint:check && npm run deps_graph:check_cycles"
},
"private": true,
"dependencies": {
@@ -43,8 +44,6 @@
"dateformat": "^5.0.3",
"gl-matrix": "^3.4.3",
"html-loader": "^3.1.0",
- "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
- "html-webpack-plugin": "^5.5.0",
"html2canvas": "^1.4.1",
"jsbn": "^1.1.0",
"jsbn-rsa": "^1.0.4",
@@ -56,7 +55,7 @@
"three": "^0.143.0",
"ts-loader": "^9.3.0",
"tslib": "^2.3.0",
- "typescript": "~4.7.2",
+ "typescript": "^4.8.0",
"webgl-utils": "^1.0.1",
"webgl-utils.js": "^1.1.0",
"webpack-cli": "^4.10.0",
@@ -69,6 +68,7 @@
"@ngxs/devtools-plugin": "^3.7.4",
"@types/chrome": "^0.0.204",
"@types/dateformat": "^5.0.0",
+ "@types/gtag.js": "^0.0.13",
"@types/jasmine": "~4.3.1",
"@types/jquery": "^3.5.14",
"@types/jsbn": "^1.2.30",
@@ -78,9 +78,12 @@
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"angular2-template-loader": "^0.6.2",
+ "copy-webpack-plugin": "^11.0.0",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
+ "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
+ "html-webpack-plugin": "^5.5.0",
"jasmine": "~4.3.0",
"jasmine-core": "~4.1.0",
"karma": "~6.3.0",
diff --git a/tools/winscope/protos/udc/surfaceflinger/common.proto b/tools/winscope/protos/udc/surfaceflinger/common.proto
new file mode 100644
index 0000000..a6d8d61
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/common.proto
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+package android.surfaceflinger;
+
+message RegionProto {
+ reserved 1; // Previously: uint64 id
+ repeated RectProto rect = 2;
+}
+
+message RectProto {
+ int32 left = 1;
+ int32 top = 2;
+ int32 right = 3;
+ int32 bottom = 4;
+}
+
+message SizeProto {
+ int32 w = 1;
+ int32 h = 2;
+}
+
+message TransformProto {
+ float dsdx = 1;
+ float dtdx = 2;
+ float dsdy = 3;
+ float dtdy = 4;
+ int32 type = 5;
+}
+
+message ColorProto {
+ float r = 1;
+ float g = 2;
+ float b = 3;
+ float a = 4;
+}
+
+message InputWindowInfoProto {
+ uint32 layout_params_flags = 1;
+ int32 layout_params_type = 2;
+ RectProto frame = 3;
+ RegionProto touchable_region = 4;
+
+ int32 surface_inset = 5;
+ bool visible = 6;
+ bool can_receive_keys = 7 [deprecated = true];
+ bool focusable = 8;
+ bool has_wallpaper = 9;
+
+ float global_scale_factor = 10;
+ float window_x_scale = 11 [deprecated = true];
+ float window_y_scale = 12 [deprecated = true];
+
+ int32 crop_layer_id = 13;
+ bool replace_touchable_region_with_crop = 14;
+ RectProto touchable_region_crop = 15;
+ TransformProto transform = 16;
+}
+
+message BlurRegion {
+ uint32 blur_radius = 1;
+ uint32 corner_radius_tl = 2;
+ uint32 corner_radius_tr = 3;
+ uint32 corner_radius_bl = 4;
+ float corner_radius_br = 5;
+ float alpha = 6;
+ int32 left = 7;
+ int32 top = 8;
+ int32 right = 9;
+ int32 bottom = 10;
+}
+
+message ColorTransformProto {
+ // This will be a 4x4 matrix of float values
+ repeated float val = 1;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/display.proto b/tools/winscope/protos/udc/surfaceflinger/display.proto
new file mode 100644
index 0000000..18e43ed
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/display.proto
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger;
+
+message DisplayProto {
+ uint64 id = 1;
+
+ string name = 2;
+
+ uint32 layer_stack = 3;
+
+ SizeProto size = 4;
+
+ RectProto layer_stack_space_rect = 5;
+
+ TransformProto transform = 6;
+
+ bool is_virtual = 7;
+
+ double dpi_x = 8;
+ double dpi_y = 9;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/layers.proto b/tools/winscope/protos/udc/surfaceflinger/layers.proto
new file mode 100644
index 0000000..8ee2757
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/layers.proto
@@ -0,0 +1,171 @@
+// Definitions for SurfaceFlinger layers.
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger;
+
+// Contains a list of all layers.
+message LayersProto {
+ repeated LayerProto layers = 1;
+}
+
+// Must match definition in the IComposerClient HAL
+enum HwcCompositionType {
+ // Invalid composition type
+ INVALID = 0;
+ // Layer was composited by the client into the client target buffer
+ CLIENT = 1;
+ // Layer was composited by the device through hardware overlays
+ DEVICE = 2;
+ // Layer was composited by the device using a color
+ SOLID_COLOR = 3;
+ // Similar to DEVICE, but the layer position may have been asynchronously set
+ // through setCursorPosition
+ CURSOR = 4;
+ // Layer was composited by the device via a sideband stream.
+ SIDEBAND = 5;
+}
+
+// Information about each layer.
+message LayerProto {
+ // unique id per layer.
+ int32 id = 1;
+ // unique name per layer.
+ string name = 2;
+ // list of children this layer may have. May be empty.
+ repeated int32 children = 3;
+ // list of layers that are z order relative to this layer.
+ repeated int32 relatives = 4;
+ // The type of layer, ex Color, Layer
+ string type = 5;
+ RegionProto transparent_region = 6;
+ RegionProto visible_region = 7;
+ RegionProto damage_region = 8;
+ uint32 layer_stack = 9;
+ // The layer's z order. Can be z order in layer stack, relative to parent,
+ // or relative to another layer specified in zOrderRelative.
+ int32 z = 10;
+ // The layer's position on the display.
+ PositionProto position = 11;
+ // The layer's requested position.
+ PositionProto requested_position = 12;
+ // The layer's size.
+ SizeProto size = 13;
+ // The layer's crop in it's own bounds.
+ RectProto crop = 14;
+ // The layer's crop in it's parent's bounds.
+ RectProto final_crop = 15 [deprecated=true];
+ bool is_opaque = 16;
+ bool invalidate = 17;
+ string dataspace = 18;
+ string pixel_format = 19;
+ // The layer's actual color.
+ ColorProto color = 20;
+ // The layer's requested color.
+ ColorProto requested_color = 21;
+ // Can be any combination of
+ // hidden = 0x01
+ // opaque = 0x02,
+ // secure = 0x80,
+ uint32 flags = 22;
+ // The layer's actual transform
+ TransformProto transform = 23;
+ // The layer's requested transform.
+ TransformProto requested_transform = 24;
+ // The parent layer. This value can be null if there is no parent.
+ int32 parent = 25;
+ // The layer that this layer has a z order relative to. This value can be null.
+ int32 z_order_relative_of = 26;
+ // This value can be null if there's nothing to draw.
+ ActiveBufferProto active_buffer = 27;
+ // The number of frames available.
+ int32 queued_frames = 28;
+ bool refresh_pending = 29;
+ // The layer's composer backend destination frame
+ RectProto hwc_frame = 30;
+ // The layer's composer backend source crop
+ FloatRectProto hwc_crop = 31;
+ // The layer's composer backend transform
+ int32 hwc_transform = 32;
+ int32 window_type = 33 [deprecated=true];
+ int32 app_id = 34 [deprecated=true];
+ // The layer's composition type
+ HwcCompositionType hwc_composition_type = 35;
+ // If it's a buffer layer, indicate if the content is protected
+ bool is_protected = 36;
+ // Current frame number being rendered.
+ uint64 curr_frame = 37;
+ // A list of barriers that the layer is waiting to update state.
+ repeated BarrierLayerProto barrier_layer = 38;
+ // If active_buffer is not null, record its transform.
+ TransformProto buffer_transform = 39;
+ int32 effective_scaling_mode = 40;
+ // Layer's corner radius.
+ float corner_radius = 41;
+ // Metadata map. May be empty.
+ map<int32, bytes> metadata = 42;
+
+ TransformProto effective_transform = 43;
+ FloatRectProto source_bounds = 44;
+ FloatRectProto bounds = 45;
+ FloatRectProto screen_bounds = 46;
+
+ InputWindowInfoProto input_window_info = 47;
+
+ // Crop used to draw the rounded corner.
+ FloatRectProto corner_radius_crop = 48;
+
+ // length of the shadow to draw around the layer, it may be set on the
+ // layer or set by a parent layer.
+ float shadow_radius = 49;
+ ColorTransformProto color_transform = 50;
+
+ bool is_relative_of = 51;
+ // Layer's background blur radius in pixels.
+ int32 background_blur_radius = 52;
+
+ uint32 owner_uid = 53;
+
+ // Regions of a layer, where blur should be applied.
+ repeated BlurRegion blur_regions = 54;
+
+ bool is_trusted_overlay = 55;
+
+ // Corner radius explicitly set on layer rather than inherited
+ float requested_corner_radius = 56;
+
+ RectProto destination_frame = 57;
+
+ uint32 original_id = 58;
+}
+
+message PositionProto {
+ float x = 1;
+ float y = 2;
+}
+
+message FloatRectProto {
+ float left = 1;
+ float top = 2;
+ float right = 3;
+ float bottom = 4;
+}
+
+message ActiveBufferProto {
+ uint32 width = 1;
+ uint32 height = 2;
+ uint32 stride = 3;
+ int32 format = 4;
+ uint64 usage = 5;
+}
+
+message BarrierLayerProto {
+ // layer id the barrier is waiting on.
+ int32 id = 1;
+ // frame number the barrier is waiting on.
+ uint64 frame_number = 2;
+}
+
diff --git a/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto b/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto
new file mode 100644
index 0000000..9702f76
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/layerstrace.proto
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/layers.proto";
+import "protos/udc/surfaceflinger/display.proto";
+
+package android.surfaceflinger;
+
+/* represents a file full of surface flinger trace entries.
+ Encoded, it should start with 0x4c 0x59 0x52 0x54 0x52 0x41 0x43 0x45 (.LYRTRACE), such
+ that they can be easily identified. */
+message LayersTraceFileProto {
+
+ /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+ (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+ constants into .proto files. */
+ enum MagicNumber {
+ INVALID = 0;
+ MAGIC_NUMBER_L = 0x5452594c; /* LYRT (little-endian ASCII) */
+ MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+ }
+
+ optional fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */
+ repeated LayersTraceProto entry = 2;
+
+ /* offset between real-time clock and elapsed time clock in nanoseconds.
+ Calculated as: systemTime(SYSTEM_TIME_REALTIME) - systemTime(SYSTEM_TIME_MONOTONIC) */
+ optional fixed64 real_to_elapsed_time_offset_nanos = 3;
+}
+
+/* one layers trace entry. */
+message LayersTraceProto {
+ /* required: elapsed realtime in nanos since boot of when this entry was logged */
+ optional sfixed64 elapsed_realtime_nanos = 1;
+
+ /* where the trace originated */
+ optional string where = 2;
+
+ optional LayersProto layers = 3;
+
+ // Blob for the current HWC information for all layers, reported by dumpsys.
+ optional string hwc_blob = 4;
+
+ /* Includes state sent during composition like visible region and composition type. */
+ optional bool excludes_composition_state = 5;
+
+ /* Number of missed entries since the last entry was recorded. */
+ optional uint32 missed_entries = 6;
+
+ repeated DisplayProto displays = 7;
+
+ optional int64 vsync_id = 8;
+}
diff --git a/tools/winscope/protos/udc/surfaceflinger/transactions.proto b/tools/winscope/protos/udc/surfaceflinger/transactions.proto
new file mode 100644
index 0000000..fd8de63
--- /dev/null
+++ b/tools/winscope/protos/udc/surfaceflinger/transactions.proto
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+syntax = "proto3";
+option optimize_for = LITE_RUNTIME;
+
+import "protos/udc/surfaceflinger/common.proto";
+
+package android.surfaceflinger.proto;
+
+/* Represents a file full of surface flinger transactions.
+ Encoded, it should start with 0x54 0x4E 0x58 0x54 0x52 0x41 0x43 0x45 (.TNXTRACE), such
+ that they can be easily identified. */
+message TransactionTraceFile {
+ /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L
+ (this is needed because enums have to be 32 bits and there's no nice way to put 64bit
+ constants into .proto files. */
+ enum MagicNumber {
+ INVALID = 0;
+ MAGIC_NUMBER_L = 0x54584E54; /* TNXT (little-endian ASCII) */
+ MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */
+ }
+
+ fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */
+ repeated TransactionTraceEntry entry = 2;
+
+ /* offset between real-time clock and elapsed time clock in nanoseconds.
+ Calculated as: systemTime(SYSTEM_TIME_REALTIME) - systemTime(SYSTEM_TIME_MONOTONIC) */
+ fixed64 real_to_elapsed_time_offset_nanos = 3;
+ uint32 version = 4;
+}
+
+message TransactionTraceEntry {
+ int64 elapsed_realtime_nanos = 1;
+ int64 vsync_id = 2;
+ repeated TransactionState transactions = 3;
+ repeated LayerCreationArgs added_layers = 4;
+ repeated uint32 destroyed_layers = 5;
+ repeated DisplayState added_displays = 6;
+ repeated int32 removed_displays = 7;
+ repeated uint32 destroyed_layer_handles = 8;
+ bool displays_changed = 9;
+ repeated DisplayInfo displays = 10;
+}
+
+message DisplayInfo {
+ uint32 layer_stack = 1;
+ int32 display_id = 2;
+ int32 logical_width = 3;
+ int32 logical_height = 4;
+ Transform transform_inverse = 5;
+ Transform transform = 6;
+ bool receives_input = 7;
+ bool is_secure = 8;
+ bool is_primary = 9;
+ bool is_virtual = 10;
+ int32 rotation_flags = 11;
+ int32 transform_hint = 12;
+
+}
+
+message LayerCreationArgs {
+ uint32 layer_id = 1;
+ string name = 2;
+ uint32 flags = 3;
+ uint32 parent_id = 4;
+ uint32 mirror_from_id = 5;
+ bool add_to_root = 6;
+ uint32 layer_stack_to_mirror = 7;
+}
+
+message Transform {
+ float dsdx = 1;
+ float dtdx = 2;
+ float dtdy = 3;
+ float dsdy = 4;
+ float tx = 5;
+ float ty = 6;
+}
+
+message TransactionState {
+ int32 pid = 1;
+ int32 uid = 2;
+ int64 vsync_id = 3;
+ int32 input_event_id = 4;
+ int64 post_time = 5;
+ uint64 transaction_id = 6;
+ repeated LayerState layer_changes = 7;
+ repeated DisplayState display_changes = 8;
+ repeated uint64 merged_transaction_ids = 9;
+}
+
+// Keep insync with layer_state_t
+message LayerState {
+ uint32 layer_id = 1;
+ // Changes are split into ChangesLsb and ChangesMsb. First 32 bits are in ChangesLsb
+ // and the next 32 bits are in ChangesMsb. This is needed because enums have to be
+ // 32 bits and there's no nice way to put 64bit constants into .proto files.
+ enum ChangesLsb {
+ eChangesLsbNone = 0;
+ ePositionChanged = 0x00000001;
+ eLayerChanged = 0x00000002;
+ // unused = 0x00000004;
+ eAlphaChanged = 0x00000008;
+
+ eMatrixChanged = 0x00000010;
+ eTransparentRegionChanged = 0x00000020;
+ eFlagsChanged = 0x00000040;
+ eLayerStackChanged = 0x00000080;
+
+ eReleaseBufferListenerChanged = 0x00000400;
+ eShadowRadiusChanged = 0x00000800;
+
+ eBufferCropChanged = 0x00002000;
+ eRelativeLayerChanged = 0x00004000;
+ eReparent = 0x00008000;
+
+ eColorChanged = 0x00010000;
+ eBufferTransformChanged = 0x00040000;
+ eTransformToDisplayInverseChanged = 0x00080000;
+
+ eCropChanged = 0x00100000;
+ eBufferChanged = 0x00200000;
+ eAcquireFenceChanged = 0x00400000;
+ eDataspaceChanged = 0x00800000;
+
+ eHdrMetadataChanged = 0x01000000;
+ eSurfaceDamageRegionChanged = 0x02000000;
+ eApiChanged = 0x04000000;
+ eSidebandStreamChanged = 0x08000000;
+
+ eColorTransformChanged = 0x10000000;
+ eHasListenerCallbacksChanged = 0x20000000;
+ eInputInfoChanged = 0x40000000;
+ eCornerRadiusChanged = -2147483648; // 0x80000000; (proto stores enums as signed int)
+ };
+ enum ChangesMsb {
+ eChangesMsbNone = 0;
+ eDestinationFrameChanged = 0x1;
+ eCachedBufferChanged = 0x2;
+ eBackgroundColorChanged = 0x4;
+ eMetadataChanged = 0x8;
+ eColorSpaceAgnosticChanged = 0x10;
+ eFrameRateSelectionPriority = 0x20;
+ eFrameRateChanged = 0x40;
+ eBackgroundBlurRadiusChanged = 0x80;
+ eProducerDisconnect = 0x100;
+ eFixedTransformHintChanged = 0x200;
+ eFrameNumberChanged = 0x400;
+ eBlurRegionsChanged = 0x800;
+ eAutoRefreshChanged = 0x1000;
+ eStretchChanged = 0x2000;
+ eTrustedOverlayChanged = 0x4000;
+ eDropInputModeChanged = 0x8000;
+ };
+ uint64 what = 2;
+ float x = 3;
+ float y = 4;
+ int32 z = 5;
+ uint32 w = 6;
+ uint32 h = 7;
+ uint32 layer_stack = 8;
+
+ enum Flags {
+ eFlagsNone = 0;
+ eLayerHidden = 0x01;
+ eLayerOpaque = 0x02;
+ eLayerSkipScreenshot = 0x40;
+ eLayerSecure = 0x80;
+ eEnableBackpressure = 0x100;
+ eLayerIsDisplayDecoration = 0x200;
+ };
+ uint32 flags = 9;
+ uint32 mask = 10;
+
+ message Matrix22 {
+ float dsdx = 1;
+ float dtdx = 2;
+ float dtdy = 3;
+ float dsdy = 4;
+ };
+ Matrix22 matrix = 11;
+ float corner_radius = 12;
+ uint32 background_blur_radius = 13;
+ uint32 parent_id = 14;
+ uint32 relative_parent_id = 15;
+
+ float alpha = 16;
+ message Color3 {
+ float r = 1;
+ float g = 2;
+ float b = 3;
+ }
+ Color3 color = 17;
+ RegionProto transparent_region = 18;
+ uint32 transform = 19;
+ bool transform_to_display_inverse = 20;
+ RectProto crop = 21;
+
+ message BufferData {
+ uint64 buffer_id = 1;
+ uint32 width = 2;
+ uint32 height = 3;
+ uint64 frame_number = 4;
+
+ enum BufferDataChange {
+ BufferDataChangeNone = 0;
+ fenceChanged = 0x01;
+ frameNumberChanged = 0x02;
+ cachedBufferChanged = 0x04;
+ }
+ uint32 flags = 5;
+ uint64 cached_buffer_id = 6;
+
+ enum PixelFormat {
+ PIXEL_FORMAT_UNKNOWN = 0;
+ PIXEL_FORMAT_CUSTOM = -4;
+ PIXEL_FORMAT_TRANSLUCENT = -3;
+ PIXEL_FORMAT_TRANSPARENT = -2;
+ PIXEL_FORMAT_OPAQUE = -1;
+ PIXEL_FORMAT_RGBA_8888 = 1;
+ PIXEL_FORMAT_RGBX_8888 = 2;
+ PIXEL_FORMAT_RGB_888 = 3;
+ PIXEL_FORMAT_RGB_565 = 4;
+ PIXEL_FORMAT_BGRA_8888 = 5;
+ PIXEL_FORMAT_RGBA_5551 = 6;
+ PIXEL_FORMAT_RGBA_4444 = 7;
+ PIXEL_FORMAT_RGBA_FP16 = 22;
+ PIXEL_FORMAT_RGBA_1010102 = 43;
+ PIXEL_FORMAT_R_8 = 0x38;
+ }
+ PixelFormat pixel_format = 7;
+ uint64 usage = 8;
+ }
+ BufferData buffer_data = 22;
+ int32 api = 23;
+ bool has_sideband_stream = 24;
+ ColorTransformProto color_transform = 25;
+ repeated BlurRegion blur_regions = 26;
+
+ message WindowInfo {
+ uint32 layout_params_flags = 1;
+ int32 layout_params_type = 2;
+ RegionProto touchable_region = 3;
+ int32 surface_inset = 4;
+ bool focusable = 5;
+ bool has_wallpaper = 6;
+ float global_scale_factor = 7;
+ uint32 crop_layer_id = 8;
+ bool replace_touchable_region_with_crop = 9;
+ RectProto touchable_region_crop = 10;
+ Transform transform = 11;
+ }
+ WindowInfo window_info_handle = 27;
+ float bg_color_alpha = 28;
+ int32 bg_color_dataspace = 29;
+ bool color_space_agnostic = 30;
+ float shadow_radius = 31;
+ int32 frame_rate_selection_priority = 32;
+ float frame_rate = 33;
+ int32 frame_rate_compatibility = 34;
+ int32 change_frame_rate_strategy = 35;
+ uint32 fixed_transform_hint = 36;
+ uint64 frame_number = 37;
+ bool auto_refresh = 38;
+ bool is_trusted_overlay = 39;
+ RectProto buffer_crop = 40;
+ RectProto destination_frame = 41;
+
+ enum DropInputMode {
+ NONE = 0;
+ ALL = 1;
+ OBSCURED = 2;
+ };
+ DropInputMode drop_input_mode = 42;
+}
+
+message DisplayState {
+ enum Changes {
+ eChangesNone = 0;
+ eSurfaceChanged = 0x01;
+ eLayerStackChanged = 0x02;
+ eDisplayProjectionChanged = 0x04;
+ eDisplaySizeChanged = 0x08;
+ eFlagsChanged = 0x10;
+ };
+ int32 id = 1;
+ uint32 what = 2;
+ uint32 flags = 3;
+ uint32 layer_stack = 4;
+ uint32 orientation = 5;
+ RectProto layer_stack_space_rect = 6;
+ RectProto oriented_display_space_rect = 7;
+ uint32 width = 8;
+ uint32 height = 9;
+}
diff --git a/tools/winscope/protractor.config.js b/tools/winscope/protractor.config.js
index 875c38f..708f555 100644
--- a/tools/winscope/protractor.config.js
+++ b/tools/winscope/protractor.config.js
@@ -21,7 +21,7 @@
// and change the hardcoded version here
exports.config = {
- specs: ['dist/e2e_test/e2e/*_test.js'],
+ specs: ['dist/e2e_test/*_test.js'],
directConnect: true,
capabilities: {
@@ -30,7 +30,7 @@
args: ['--headless', '--disable-gpu', '--window-size=1280x1024'],
},
},
- chromeDriver: './node_modules/webdriver-manager/selenium/chromedriver_114.0.5735.90',
+ chromeDriver: './deps_build/chromedriver-linux64/chromedriver',
allScriptsTimeout: 10000,
getPageTimeout: 10000,
diff --git a/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol.ts b/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol.ts
index 787cc2e..e1f8d5c 100644
--- a/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol.ts
+++ b/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol.ts
@@ -16,44 +16,43 @@
import {FunctionUtils} from 'common/function_utils';
import {
- BuganizerAttachmentsDownloadEmitter,
- OnBuganizerAttachmentsDownloaded,
- OnBuganizerAttachmentsDownloadStart,
-} from 'interfaces/buganizer_attachments_download_emitter';
+ BuganizerAttachmentsDownloaded,
+ BuganizerAttachmentsDownloadStart,
+ WinscopeEvent,
+ WinscopeEventType,
+} from 'messaging/winscope_event';
+import {EmitEvent, WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {MessageType, OpenBuganizerResponse, OpenRequest, WebCommandMessage} from './messages';
-export class AbtChromeExtensionProtocol implements BuganizerAttachmentsDownloadEmitter {
+export class AbtChromeExtensionProtocol implements WinscopeEventEmitter, WinscopeEventListener {
static readonly ABT_EXTENSION_ID = 'mbbaofdfoekifkfpgehgffcpagbbjkmj';
- private onAttachmentsDownloadStart: OnBuganizerAttachmentsDownloadStart =
- FunctionUtils.DO_NOTHING;
- private onAttachmentsDownloaded: OnBuganizerAttachmentsDownloaded =
- FunctionUtils.DO_NOTHING_ASYNC;
- setOnBuganizerAttachmentsDownloadStart(callback: OnBuganizerAttachmentsDownloadStart) {
- this.onAttachmentsDownloadStart = callback;
+ private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
+
+ setEmitEvent(callback: EmitEvent) {
+ this.emitEvent = callback;
}
- setOnBuganizerAttachmentsDownloaded(callback: OnBuganizerAttachmentsDownloaded) {
- this.onAttachmentsDownloaded = callback;
- }
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.APP_INITIALIZED, async () => {
+ const urlParams = new URLSearchParams(window.location.search);
+ if (urlParams.get('source') !== 'openFromExtension' || !chrome) {
+ return;
+ }
- run() {
- const urlParams = new URLSearchParams(window.location.search);
- if (urlParams.get('source') !== 'openFromExtension' || !chrome) {
- return;
- }
+ await this.emitEvent(new BuganizerAttachmentsDownloadStart());
- this.onAttachmentsDownloadStart();
+ const openRequestMessage: OpenRequest = {
+ action: MessageType.OPEN_REQUEST,
+ };
- const openRequestMessage: OpenRequest = {
- action: MessageType.OPEN_REQUEST,
- };
-
- chrome.runtime.sendMessage(
- AbtChromeExtensionProtocol.ABT_EXTENSION_ID,
- openRequestMessage,
- async (message) => await this.onMessageReceived(message)
- );
+ chrome.runtime.sendMessage(
+ AbtChromeExtensionProtocol.ABT_EXTENSION_ID,
+ openRequestMessage,
+ async (message) => await this.onMessageReceived(message)
+ );
+ });
}
private async onMessageReceived(message: WebCommandMessage) {
@@ -84,7 +83,7 @@
});
const files = await Promise.all(filesBlobPromises);
- await this.onAttachmentsDownloaded(files);
+ await this.emitEvent(new BuganizerAttachmentsDownloaded(files));
}
private isOpenBuganizerResponseMessage(
diff --git a/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts b/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts
deleted file mode 100644
index c07e976..0000000
--- a/tools/winscope/src/abt_chrome_extension/abt_chrome_extension_protocol_stub.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.
- */
-
-import {FunctionUtils} from 'common/function_utils';
-import {
- BuganizerAttachmentsDownloadEmitter,
- OnBuganizerAttachmentsDownloaded,
- OnBuganizerAttachmentsDownloadStart,
-} from 'interfaces/buganizer_attachments_download_emitter';
-import {Runnable} from 'interfaces/runnable';
-
-export class AbtChromeExtensionProtocolStub
- implements BuganizerAttachmentsDownloadEmitter, Runnable
-{
- onBuganizerAttachmentsDownloadStart: OnBuganizerAttachmentsDownloadStart =
- FunctionUtils.DO_NOTHING;
- onBuganizerAttachmentsDownloaded: OnBuganizerAttachmentsDownloaded =
- FunctionUtils.DO_NOTHING_ASYNC;
-
- setOnBuganizerAttachmentsDownloadStart(callback: OnBuganizerAttachmentsDownloadStart) {
- this.onBuganizerAttachmentsDownloadStart = callback;
- }
-
- setOnBuganizerAttachmentsDownloaded(callback: OnBuganizerAttachmentsDownloaded) {
- this.onBuganizerAttachmentsDownloaded = callback;
- }
-
- run() {
- // do nothing
- }
-}
diff --git a/tools/winscope/src/adb/winscope_proxy.py b/tools/winscope/src/adb/winscope_proxy.py
index 15f606c..20c37fc 100644
--- a/tools/winscope/src/adb/winscope_proxy.py
+++ b/tools/winscope/src/adb/winscope_proxy.py
@@ -23,6 +23,8 @@
# run: python3 winscope_proxy.py
#
+import argparse
+import base64
import json
import logging
import os
@@ -37,17 +39,55 @@
from enum import Enum
from http import HTTPStatus
from http.server import HTTPServer, BaseHTTPRequestHandler
+from logging import DEBUG, INFO, WARNING
from tempfile import NamedTemporaryFile
-import base64
+
+# GLOBALS #
+
+log = None
+secret_token = None
# CONFIG #
-LOG_LEVEL = logging.DEBUG
+def create_argument_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description='Proxy for go/winscope', prog='winscope_proxy')
-PORT = 5544
+ parser.add_argument('--verbose', '-v', dest='loglevel', action='store_const', const=INFO)
+ parser.add_argument('--debug', '-d', dest='loglevel', action='store_const', const=DEBUG)
+ parser.add_argument('--port', '-p', default=5544, action='store')
+
+ parser.set_defaults(loglevel=WARNING)
+
+ return parser
# Keep in sync with ProxyClient#VERSION in Winscope
-VERSION = '1.0'
+VERSION = '1.2'
+
+PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
+PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf'
+PERFETTO_SF_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy.surfaceflinger.conf'
+PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'
+PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'
+PERFETTO_UTILS = """
+function is_perfetto_data_source_available {
+ local data_source_name=$1
+ if perfetto --query | grep $data_source_name 2>&1 >/dev/null; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+function is_any_perfetto_data_source_available {
+ if is_perfetto_data_source_available android.surfaceflinger.layers || \
+ is_perfetto_data_source_available android.surfaceflinger.transactions || \
+ is_perfetto_data_source_available com.android.wm.shell.transition; then
+ return 0
+ else
+ return 1
+ fi
+}
+"""
WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
WINSCOPE_TOKEN_HEADER = "Winscope-Token"
@@ -66,11 +106,6 @@
# Max interval between the client keep-alive requests in seconds
KEEP_ALIVE_INTERVAL_S = 5
-logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-log = logging.getLogger("ADBProxy")
-
-
class File:
def __init__(self, file, filetype) -> None:
self.file = file
@@ -132,23 +167,37 @@
self.trace_start = trace_start
self.trace_stop = trace_stop
+
# Order of files matters as they will be expected in that order and decoded in that order
TRACE_TARGETS = {
+ "view_capture_trace": TraceTarget(
+ File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
+ 'su root settings put global view_capture_enabled 1\necho "View capture trace started."',
+ "su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip'; su root settings put global view_capture_enabled 0"
+ ),
"window_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
'su root cmd window tracing start\necho "WM trace started."',
'su root cmd window tracing stop >/dev/null 2>&1'
),
- "accessibility_trace": TraceTarget(
- WinscopeFileMatcher("/data/misc/a11ytrace", "a11y_trace", "accessibility_trace"),
- 'su root cmd accessibility start-trace\necho "Accessibility trace started."',
- 'su root cmd accessibility stop-trace >/dev/null 2>&1'
- ),
"layers_trace": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
- 'su root service call SurfaceFlinger 1025 i32 1\necho "SF trace started."',
- 'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1'
- ),
+ f"""
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+ cat {PERFETTO_SF_CONFIG_FILE} >> {PERFETTO_TRACE_CONFIG_FILE}
+ echo 'SF trace (perfetto) configured to start along the other perfetto traces'
+else
+ su root service call SurfaceFlinger 1025 i32 1
+ echo 'SF layers trace (legacy) started'
+fi
+ """,
+ """
+if ! is_perfetto_data_source_available android.surfaceflinger.layers; then
+ su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
+ echo 'SF layers trace (legacy) stopped.'
+fi
+"""
+),
"screen_recording": TraceTarget(
File(f'/data/local/tmp/screen.mp4', "screen_recording"),
f'screenrecord --bit-rate 8M /data/local/tmp/screen.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
@@ -156,8 +205,29 @@
),
"transactions": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
- 'su root service call SurfaceFlinger 1041 i32 1\necho "SF transactions recording started."',
- 'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1'
+ f"""
+if is_perfetto_data_source_available android.surfaceflinger.transaction; then
+ cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+data_sources: {{
+ config {{
+ name: "android.surfaceflinger.transactions"
+ surfaceflinger_transactions_config: {{
+ mode: MODE_ACTIVE
+ }}
+ }}
+}}
+EOF
+ echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
+else
+ su root service call SurfaceFlinger 1041 i32 1
+ echo 'SF transactions trace (legacy) started'
+fi
+""",
+ """
+if ! is_perfetto_data_source_available android.surfaceflinger.transaction; then
+ su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1
+fi
+"""
),
"transactions_legacy": TraceTarget(
[
@@ -200,9 +270,55 @@
"transition_traces": TraceTarget(
[WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"),
WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")],
- 'su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start\necho "Transition traces started."',
- 'su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1'
+ f"""
+if is_perfetto_data_source_available com.android.wm.shell.transition; then
+ cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+data_sources: {{
+ config {{
+ name: "com.android.wm.shell.transition"
+ }}
+}}
+EOF
+ echo 'Transition trace (perfetto) configured to start along the other perfetto traces'
+else
+ su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
+ echo "Transition traces (legacy) started."
+fi
+ """,
+ """
+if ! is_perfetto_data_source_available com.android.wm.shell.transition; then
+ su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1
+ echo 'Transition traces (legacy) stopped.'
+fi
+"""
),
+ "perfetto_trace": TraceTarget(
+ File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
+ f"""
+if is_any_perfetto_data_source_available; then
+ cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+buffers: {{
+ size_kb: 50000
+ fill_policy: RING_BUFFER
+}}
+duration_ms: 0
+flush_period_ms: 1000
+write_into_file: true
+max_file_size_bytes: 1000000000
+EOF
+
+ rm -f {PERFETTO_TRACE_FILE}
+ perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
+ echo 'Started perfetto trace'
+fi
+""",
+ """
+if is_any_perfetto_data_source_available; then
+ perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
+ echo 'Stopped perfetto trace'
+fi
+""",
+ )
}
@@ -211,16 +327,43 @@
"""
def __init__(self) -> None:
- self.flags = 0
+ self.flags = []
+ self.perfetto_flags = []
def add(self, config: str) -> None:
- self.flags |= CONFIG_FLAG[config]
+ self.flags.append(config)
def is_valid(self, config: str) -> bool:
- return config in CONFIG_FLAG
+ return config in SF_LEGACY_FLAGS_MAP
def command(self) -> str:
- return f'su root service call SurfaceFlinger 1033 i32 {self.flags}'
+ legacy_flags = 0
+ for flag in self.flags:
+ legacy_flags |= SF_LEGACY_FLAGS_MAP[flag]
+
+ perfetto_flags = "\n".join([f"""trace_flags: {SF_PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags])
+
+ return f"""
+{PERFETTO_UTILS}
+
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+ cat << EOF > {PERFETTO_SF_CONFIG_FILE}
+data_sources: {{
+ config {{
+ name: "android.surfaceflinger.layers"
+ surfaceflinger_layers_config: {{
+ mode: MODE_ACTIVE
+ {perfetto_flags}
+ }}
+ }}
+}}
+EOF
+ echo 'SF trace (perfetto) configured.'
+else
+ su root service call SurfaceFlinger 1033 i32 {legacy_flags}
+ echo 'SF trace (legacy) configured'
+fi
+"""
class SurfaceFlingerTraceSelectedConfig:
"""Handles optional selected configuration for surfaceflinger traces.
@@ -269,7 +412,7 @@
return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
-CONFIG_FLAG = {
+SF_LEGACY_FLAGS_MAP = {
"input": 1 << 1,
"composition": 1 << 2,
"metadata": 1 << 3,
@@ -278,6 +421,15 @@
"virtualdisplays": 1 << 6
}
+SF_PERFETTO_FLAGS_MAP = {
+ "input": "TRACE_FLAG_INPUT",
+ "composition": "TRACE_FLAG_COMPOSITION",
+ "metadata": "TRACE_FLAG_EXTRA",
+ "hwc": "TRACE_FLAG_HWC",
+ "tracebuffers": "TRACE_FLAG_BUFFERS",
+ "virtualdisplays": "TRACE_FLAG_VIRTUAL_DISPLAYS",
+}
+
#Keep up to date with options in DataAdb.vue
CONFIG_SF_SELECTION = [
"sfbuffersize",
@@ -310,9 +462,50 @@
File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
),
+
"layers_dump": DumpTarget(
File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
- f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}'
+ f"""
+if is_perfetto_data_source_available android.surfaceflinger.layers; then
+ cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+data_sources: {{
+ config {{
+ name: "android.surfaceflinger.layers"
+ surfaceflinger_layers_config: {{
+ mode: MODE_DUMP
+ trace_flags: TRACE_FLAG_INPUT
+ trace_flags: TRACE_FLAG_COMPOSITION
+ trace_flags: TRACE_FLAG_HWC
+ trace_flags: TRACE_FLAG_BUFFERS
+ trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS
+ }}
+ }}
+}}
+EOF
+ echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces'
+else
+ su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
+fi
+"""
+ ),
+
+ "perfetto_dump": DumpTarget(
+ File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"),
+ f"""
+if is_any_perfetto_data_source_available; then
+ cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+buffers: {{
+ size_kb: 50000
+ fill_policy: RING_BUFFER
+}}
+duration_ms: 1
+EOF
+
+ rm -f {PERFETTO_DUMP_FILE}
+ perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
+ echo 'Recorded perfetto dump'
+fi
+ """
)
}
@@ -343,9 +536,6 @@
return token
-secret_token = get_token()
-
-
class RequestType(Enum):
GET = 1
POST = 2
@@ -481,7 +671,7 @@
class ListDevicesEndpoint(RequestEndpoint):
- ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
+ ADB_INFO_RE = re.compile("^([A-Za-z0-9._:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
_foundDevices = None
def process(self, server, path):
@@ -516,6 +706,18 @@
raise BadRequest("Content length unreadable\n" + str(err))
return json.loads(server.rfile.read(length).decode("utf-8"))
+ def move_perfetto_target_to_end_of_list(self, targets):
+ # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will
+ # be processed last.
+ # A perfetto target must be processed last, so that perfetto tracing is started only after
+ # the other targets have been processed and have configured the perfetto config file.
+ def is_perfetto_target(target):
+ return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"]
+ non_perfetto_targets = [t for t in targets if not is_perfetto_target(t)]
+ perfetto_targets = [t for t in targets if is_perfetto_target(t)]
+ return non_perfetto_targets + perfetto_targets
+
+
class FetchFilesEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
@@ -541,7 +743,7 @@
call_adb_outfile('exec-out su root cat ' +
file_path, tmp, device_id)
log.debug(f"Deleting file {file_path} from device")
- call_adb('shell su root rm ' + file_path, device_id)
+ call_adb('shell su root rm -f ' + file_path, device_id)
log.debug(f"Uploading file {tmp.name}")
if file_type not in file_buffers:
file_buffers[file_type] = []
@@ -642,6 +844,8 @@
TRACE_COMMAND = """
set -e
+{perfetto_utils}
+
echo "Starting trace..."
echo "TRACE_START" > /data/local/tmp/winscope_status
@@ -655,14 +859,17 @@
set -x
trap - EXIT HUP INT
- {}
+ {stop_commands}
echo "TRACE_OK" > /data/local/tmp/winscope_status
}}
trap stop_trace EXIT HUP INT
echo "Signal handler registered."
-{}
+# Clear perfetto config file. The start commands below are going to populate it.
+rm -f {perfetto_config_file}
+
+{start_commands}
# ADB shell does not handle hung up well and does not call HUP handler when a child is active in foreground,
# as a workaround we sleep for short intervals in a loop so the handler is called after a sleep interval.
@@ -674,6 +881,7 @@
requested_types = self.get_request(server)
log.debug(f"Clienting requested trace types {requested_types}")
requested_traces = [TRACE_TARGETS[t] for t in requested_types]
+ requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
@@ -684,8 +892,10 @@
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
device_id))
command = StartTrace.TRACE_COMMAND.format(
- '\n'.join([t.trace_stop for t in requested_traces]),
- '\n'.join([t.trace_start for t in requested_traces]))
+ perfetto_utils=PERFETTO_UTILS,
+ stop_commands='\n'.join([t.trace_stop for t in requested_traces]),
+ perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
+ start_commands='\n'.join([t.trace_start for t in requested_traces]))
log.debug("Trace requested for {} with targets {}".format(
device_id, ','.join(requested_types)))
log.debug(f"Executing command \"{command}\" on {device_id}...")
@@ -799,9 +1009,11 @@
setTracingLevel = config.setTracingLevel()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
- execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
execute_command(server, device_id, shell, "tracing type", setTracingType)
execute_command(server, device_id, shell, "tracing level", setTracingLevel)
+ # /!\ buffer size must be configured last
+ # otherwise the other configurations might override it
+ execute_command(server, device_id, shell, "wm buffer size", setBufferSize)
class StatusEndpoint(DeviceRequestEndpoint):
@@ -818,6 +1030,7 @@
try:
requested_types = self.get_request(server)
requested_traces = [DUMP_TARGETS[t] for t in requested_types]
+ requested_traces = self.move_perfetto_target_to_end_of_list(requested_traces)
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
@@ -826,7 +1039,15 @@
raise AdbError(
"Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
.format(device_id))
- command = '\n'.join(t.dump_command for t in requested_traces)
+ dump_commands = '\n'.join(t.dump_command for t in requested_traces)
+ command = f"""
+{PERFETTO_UTILS}
+
+# Clear perfetto config file. The commands below are going to populate it.
+rm -f {PERFETTO_DUMP_CONFIG_FILE}
+
+{dump_commands}
+"""
shell = ['adb', '-s', device_id, 'shell']
log.debug("Starting dump shell {}".format(' '.join(shell)))
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
@@ -887,10 +1108,19 @@
if __name__ == '__main__':
+ args = create_argument_parser().parse_args()
+
+ logging.basicConfig(stream=sys.stderr, level=args.loglevel,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+ log = logging.getLogger("ADBProxy")
+ secret_token = get_token()
+
print("Winscope ADB Connect proxy version: " + VERSION)
print('Winscope token: ' + secret_token)
- httpd = HTTPServer(('localhost', PORT), ADBWinscopeProxy)
+
+ httpd = HTTPServer(('localhost', args.port), ADBWinscopeProxy)
try:
httpd.serve_forever()
except KeyboardInterrupt:
- log.info("Shutting down")
\ No newline at end of file
+ log.info("Shutting down")
diff --git a/tools/winscope/src/app/app_module.ts b/tools/winscope/src/app/app_module.ts
index 93957ad..21fc568 100644
--- a/tools/winscope/src/app/app_module.ts
+++ b/tools/winscope/src/app/app_module.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import {ClipboardModule} from '@angular/cdk/clipboard';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {ScrollingModule} from '@angular/cdk/scrolling';
import {CommonModule} from '@angular/common';
@@ -38,21 +39,22 @@
import {MatTabsModule} from '@angular/material/tabs';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatTooltipModule} from '@angular/material/tooltip';
-import {BrowserModule} from '@angular/platform-browser';
+import {BrowserModule, Title} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {CoordinatesTableComponent} from 'viewers/components/coordinates_table_component';
import {HierarchyComponent} from 'viewers/components/hierarchy_component';
import {ImeAdditionalPropertiesComponent} from 'viewers/components/ime_additional_properties_component';
import {PropertiesComponent} from 'viewers/components/properties_component';
import {PropertiesTableComponent} from 'viewers/components/properties_table_component';
-import {PropertyGroupsComponent} from 'viewers/components/property_groups_component';
import {RectsComponent} from 'viewers/components/rects/rects_component';
+import {SurfaceFlingerPropertyGroupsComponent} from 'viewers/components/surface_flinger_property_groups_component';
import {TransformMatrixComponent} from 'viewers/components/transform_matrix_component';
import {TreeComponent} from 'viewers/components/tree_component';
import {TreeNodeComponent} from 'viewers/components/tree_node_component';
import {TreeNodeDataViewComponent} from 'viewers/components/tree_node_data_view_component';
import {TreeNodePropertiesDataViewComponent} from 'viewers/components/tree_node_properties_data_view_component';
import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
+import {ViewCapturePropertyGroupsComponent} from 'viewers/components/view_capture_property_groups_component';
import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
@@ -70,9 +72,11 @@
import {CollectTracesComponent} from './components/collect_traces_component';
import {LoadProgressComponent} from './components/load_progress_component';
import {SnackBarComponent} from './components/snack_bar_component';
-import {ExpandedTimelineComponent} from './components/timeline/expanded_timeline_component';
-import {MiniTimelineComponent} from './components/timeline/mini_timeline_component';
-import {SingleTimelineComponent} from './components/timeline/single_timeline_component';
+import {DefaultTimelineRowComponent} from './components/timeline/expanded-timeline/default_timeline_row_component';
+import {ExpandedTimelineComponent} from './components/timeline/expanded-timeline/expanded_timeline_component';
+import {TransitionTimelineComponent} from './components/timeline/expanded-timeline/transition_timeline_component';
+import {MiniTimelineComponent} from './components/timeline/mini-timeline/mini_timeline_component';
+import {SliderComponent} from './components/timeline/mini-timeline/slider_component';
import {TimelineComponent} from './components/timeline/timeline_component';
import {TraceConfigComponent} from './components/trace_config_component';
import {TraceViewComponent} from './components/trace_view_component';
@@ -103,7 +107,7 @@
TreeNodeComponent,
TreeNodeDataViewComponent,
TreeNodePropertiesDataViewComponent,
- PropertyGroupsComponent,
+ SurfaceFlingerPropertyGroupsComponent,
TransformMatrixComponent,
PropertiesTableComponent,
ImeAdditionalPropertiesComponent,
@@ -111,12 +115,15 @@
TimelineComponent,
MiniTimelineComponent,
ExpandedTimelineComponent,
- SingleTimelineComponent,
+ DefaultTimelineRowComponent,
+ TransitionTimelineComponent,
SnackBarComponent,
MatDrawer,
MatDrawerContent,
MatDrawerContainer,
LoadProgressComponent,
+ SliderComponent,
+ ViewCapturePropertyGroupsComponent,
],
imports: [
BrowserModule,
@@ -145,8 +152,10 @@
MatSnackBarModule,
ScrollingModule,
DragDropModule,
+ ClipboardModule,
ReactiveFormsModule,
],
+ providers: [Title],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
bootstrap: [AppComponent],
})
diff --git a/tools/winscope/src/app/components/adb_proxy_component.ts b/tools/winscope/src/app/components/adb_proxy_component.ts
index 3f0c0a3..b9399f0 100644
--- a/tools/winscope/src/app/components/adb_proxy_component.ts
+++ b/tools/winscope/src/app/components/adb_proxy_component.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {UrlUtils} from 'common/url_utils';
import {proxyClient, ProxyClient, ProxyState} from 'trace_collection/proxy_client';
@Component({
@@ -25,21 +26,29 @@
<p class="mat-body-1">
Launch the Winscope ADB Connect proxy to capture traces directly from your browser.
</p>
- <p class="mat-body-1">Python 3.5+ and ADB are required.</p>
- <p class="mat-body-1">
- Run:
- <code>
- python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py
- </code>
- </p>
- <p class="mat-body-1">Or get it from the AOSP repository.</p>
+ <p class="mat-body-1">Python 3.5+ and ADB are required. Run this command:</p>
+ <mat-form-field class="proxy-command-form" appearance="outline">
+ <input matInput readonly [value]="proxyCommand" />
+ <button
+ mat-icon-button
+ matSuffix
+ [cdkCopyToClipboard]="proxyCommand"
+ matTooltip="Copy command">
+ <mat-icon>content_copy</mat-icon>
+ </button>
+ </mat-form-field>
+ <p class="mat-body-1">Or download below.</p>
</div>
<div class="further-adb-info-actions">
- <button color="primary" mat-stroked-button (click)="downloadFromAosp()">
- Download from AOSP
+ <button
+ class="download-proxy-btn"
+ color="primary"
+ mat-stroked-button
+ (click)="onDownloadProxyClick()">
+ Download Proxy
</button>
- <button color="primary" mat-stroked-button class="retry" (click)="restart()">
+ <button color="primary" mat-stroked-button class="retry" (click)="onRetryButtonClick()">
Retry
</button>
</div>
@@ -51,21 +60,31 @@
<mat-icon class="adb-icon">update</mat-icon>
<span class="adb-info">Your local proxy version is incompatible with Winscope.</span>
</p>
- <p class="mat-body-1">Please update the proxy to version {{ proxyVersion }}.</p>
<p class="mat-body-1">
- Run:
- <code>
- python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py
- </code>
+ Please update the proxy to version {{ proxyVersion }}. Run this command:
</p>
- <p class="mat-body-1">Or get it from the AOSP repository.</p>
+ <mat-form-field class="proxy-command-container" appearance="outline">
+ <input matInput readonly [value]="proxyCommand" />
+ <button
+ mat-icon-button
+ matSuffix
+ [cdkCopyToClipboard]="proxyCommand"
+ matTooltip="Copy command">
+ <mat-icon>content_copy</mat-icon>
+ </button>
+ </mat-form-field>
+ <p class="mat-body-1">Or download below.</p>
</div>
<div class="further-adb-info-actions">
- <button color="primary" mat-stroked-button (click)="downloadFromAosp()">
- Download from AOSP
+ <button
+ class="download-proxy-btn"
+ color="primary"
+ mat-stroked-button
+ (click)="onDownloadProxyClick()">
+ Download Proxy
</button>
- <button color="primary" mat-stroked-button class="retry" (click)="restart()">
+ <button color="primary" mat-stroked-button class="retry" (click)="onRetryButtonClick()">
Retry
</button>
</div>
@@ -78,7 +97,9 @@
<span class="adb-info">Proxy authorisation required.</span>
</p>
<p class="mat-body-1">Enter Winscope proxy token:</p>
- <mat-form-field>
+ <mat-form-field
+ class="proxy-key-input-field"
+ (keydown.enter)="onKeydownEnterProxyKeyInput($event)">
<input matInput [(ngModel)]="proxyKeyItem" name="proxy-key" />
</mat-form-field>
<p class="mat-body-1">
@@ -87,7 +108,7 @@
</div>
<div class="further-adb-info-actions">
- <button color="primary" mat-stroked-button class="retry" (click)="restart()">
+ <button color="primary" mat-stroked-button class="retry" (click)="onRetryButtonClick()">
Connect
</button>
</div>
@@ -116,6 +137,14 @@
flex-wrap: wrap;
gap: 10px;
}
+ /* TODO(b/300063426): remove after migration to angular 15, replace with subscriptSizing */
+ ::ng-deep .proxy-command-form .mat-form-field-wrapper {
+ padding: 0;
+ }
+ .proxy-command-text {
+ user-select: all;
+ overflow: auto;
+ }
.adb-info {
margin-left: 5px;
}
@@ -135,16 +164,24 @@
states = ProxyState;
proxyKeyItem = '';
readonly proxyVersion = this.proxy.VERSION;
- readonly downloadProxyUrl: string =
- 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py';
+ readonly downloadProxyUrl: string = UrlUtils.getRootUrl() + 'winscope_proxy.py';
+ readonly proxyCommand: string =
+ 'python3 $ANDROID_BUILD_TOP/development/tools/winscope/src/adb/winscope_proxy.py';
- restart() {
- this.addKey.emit(this.proxyKeyItem);
+ onRetryButtonClick() {
+ if (this.proxyKeyItem.length > 0) {
+ this.addKey.emit(this.proxyKeyItem);
+ }
this.proxy.setState(this.states.CONNECTING);
this.proxyChange.emit(this.proxy);
}
- downloadFromAosp() {
+ onKeydownEnterProxyKeyInput(event: MouseEvent) {
+ (event.target as HTMLInputElement).blur();
+ this.onRetryButtonClick();
+ }
+
+ onDownloadProxyClick() {
window.open(this.downloadProxyUrl, '_blank')?.focus();
}
}
diff --git a/tools/winscope/src/app/components/adb_proxy_component_test.ts b/tools/winscope/src/app/components/adb_proxy_component_test.ts
index f564abe..f0c024a 100644
--- a/tools/winscope/src/app/components/adb_proxy_component_test.ts
+++ b/tools/winscope/src/app/components/adb_proxy_component_test.ts
@@ -21,6 +21,7 @@
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
import {proxyClient, ProxyState} from 'trace_collection/proxy_client';
import {AdbProxyComponent} from './adb_proxy_component';
@@ -76,14 +77,60 @@
expect(htmlElement.querySelector('.adb-icon')?.innerHTML).toBe('lock');
});
- it('check retry button acts as expected', async () => {
+ it('check download proxy button downloads proxy', () => {
component.proxy.setState(ProxyState.NO_PROXY);
fixture.detectChanges();
- spyOn(component, 'restart').and.callThrough();
+ const spy = spyOn(window, 'open');
+ const button: HTMLButtonElement | null = htmlElement.querySelector('.download-proxy-btn');
+ expect(button).toBeInstanceOf(HTMLButtonElement);
+ button?.click();
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalledWith(component.downloadProxyUrl, '_blank');
+ });
+
+ it('check retry button if no proxy trys to reconnect proxy', () => {
+ component.proxy.setState(ProxyState.NO_PROXY);
+ fixture.detectChanges();
const button: HTMLButtonElement | null = htmlElement.querySelector('.retry');
expect(button).toBeInstanceOf(HTMLButtonElement);
- button?.dispatchEvent(new Event('click'));
- await fixture.whenStable();
- expect(component.restart).toHaveBeenCalled();
+ button?.click();
+ fixture.detectChanges();
+ expect(component.proxy.state).toBe(ProxyState.CONNECTING);
+ });
+
+ it('check input proxy token saved as expected', () => {
+ const spy = spyOn(component.addKey, 'emit');
+
+ component.proxy.setState(ProxyState.UNAUTH);
+ fixture.detectChanges();
+ let button: HTMLButtonElement | null = htmlElement.querySelector('.retry');
+ button?.click();
+ fixture.detectChanges();
+ expect(spy).not.toHaveBeenCalled();
+
+ component.proxy.setState(ProxyState.UNAUTH);
+ component.proxyKeyItem = '12345';
+ fixture.detectChanges();
+ button = htmlElement.querySelector('.retry');
+ button?.click();
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('retries proxy connection on enter key', async () => {
+ const spy = spyOn(component.proxyChange, 'emit');
+ component.proxy.setState(ProxyState.UNAUTH);
+ fixture.detectChanges();
+ const proxyKeyInputField = assertDefined(
+ htmlElement.querySelector('.proxy-key-input-field')
+ ) as HTMLInputElement;
+ const proxyKeyInput = assertDefined(
+ proxyKeyInputField.querySelector('input')
+ ) as HTMLInputElement;
+
+ proxyKeyInput.value = '12345';
+ proxyKeyInputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
});
});
diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts
index deb1112..6e3263d 100644
--- a/tools/winscope/src/app/components/app_component.ts
+++ b/tools/winscope/src/app/components/app_component.ts
@@ -19,10 +19,13 @@
Component,
Inject,
Injector,
+ NgZone,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {createCustomElement} from '@angular/elements';
+import {FormControl, Validators} from '@angular/forms';
+import {Title} from '@angular/platform-browser';
import {AbtChromeExtensionProtocol} from 'abt_chrome_extension/abt_chrome_extension_protocol';
import {Mediator} from 'app/mediator';
import {TimelineData} from 'app/timeline_data';
@@ -30,9 +33,19 @@
import {TracePipeline} from 'app/trace_pipeline';
import {FileUtils} from 'common/file_utils';
import {PersistentStore} from 'common/persistent_store';
+import {Timestamp} from 'common/time';
import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
-import {TraceDataListener} from 'interfaces/trace_data_listener';
-import {Timestamp} from 'trace/timestamp';
+import {CrossPlatform, NoCache} from 'flickerlib/common';
+import {
+ AppFilesCollected,
+ AppFilesUploaded,
+ AppInitialized,
+ AppResetRequest,
+ AppTraceViewRequest,
+ WinscopeEvent,
+ WinscopeEventType,
+} from 'messaging/winscope_event';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
import {proxyClient, ProxyState} from 'trace_collection/proxy_client';
@@ -48,67 +61,102 @@
import {CollectTracesComponent} from './collect_traces_component';
import {SnackBarOpener} from './snack_bar_opener';
import {TimelineComponent} from './timeline/timeline_component';
+import {TraceViewComponent} from './trace_view_component';
import {UploadTracesComponent} from './upload_traces_component';
@Component({
selector: 'app-root',
template: `
<mat-toolbar class="toolbar">
- <span class="app-title">Winscope</span>
+ <div class="horizontal-align vertical-align">
+ <span class="app-title">Winscope</span>
+ <div *ngIf="showDataLoadedElements" class="file-descriptor vertical-align">
+ <span *ngIf="!isEditingFilename" class="download-file-info mat-body-2">
+ {{ filenameFormControl.value }}.zip
+ </span>
+ <mat-form-field
+ class="file-name-input-field"
+ *ngIf="isEditingFilename"
+ floatLabel="always"
+ (keydown.enter)="onCheckIconClick()"
+ (focusout)="onCheckIconClick()"
+ matTooltip="Allowed: A-Z a-z 0-9 . _ - #">
+ <mat-label>Edit file name</mat-label>
+ <input matInput class="right-align" [formControl]="filenameFormControl" />
+ <span matSuffix>.zip</span>
+ </mat-form-field>
+ <button
+ *ngIf="isEditingFilename"
+ mat-icon-button
+ class="check-button"
+ matTooltip="Submit file name"
+ (click)="onCheckIconClick()">
+ <mat-icon>check</mat-icon>
+ </button>
+ <button
+ *ngIf="!isEditingFilename"
+ mat-icon-button
+ class="edit-button"
+ matTooltip="Edit file name"
+ (click)="onPencilIconClick()">
+ <mat-icon>edit</mat-icon>
+ </button>
+ <button
+ mat-icon-button
+ *ngIf="!isEditingFilename"
+ matTooltip="Download all traces"
+ class="save-button"
+ (click)="onDownloadTracesButtonClick()">
+ <mat-icon>download</mat-icon>
+ </button>
+ </div>
+ </div>
- <a href="http://go/winscope-legacy">
- <button color="primary" mat-button>Open legacy Winscope</button>
- </a>
-
- <div class="spacer">
+ <div class="horizontal-align vertical-align active" *ngIf="showDataLoadedElements">
<mat-icon
- *ngIf="dataLoaded && activeTrace"
+ *ngIf="activeTrace"
class="icon"
[matTooltip]="TRACE_INFO[activeTrace.type].name"
[style]="{color: TRACE_INFO[activeTrace.type].color, marginRight: '0.5rem'}">
{{ TRACE_INFO[activeTrace.type].icon }}
</mat-icon>
- <span *ngIf="dataLoaded" class="active-trace-file-info mat-body-2">
+ <span class="trace-file-info mat-body-2" [matTooltip]="activeTraceFileInfo">
{{ activeTraceFileInfo }}
</span>
</div>
- <button
- *ngIf="dataLoaded"
- color="primary"
- mat-stroked-button
- (click)="mediator.onWinscopeUploadNew()">
- Upload New
- </button>
+ <div class="horizontal-align vertical-align">
+ <button
+ *ngIf="showDataLoadedElements"
+ color="primary"
+ mat-stroked-button
+ (click)="onUploadNewButtonClick()">
+ Upload New
+ </button>
+ <button
+ mat-icon-button
+ matTooltip="Report bug"
+ (click)="goToLink('https://b.corp.google.com/issues/new?component=909476')">
+ <mat-icon>bug_report</mat-icon>
+ </button>
- <button
- mat-icon-button
- matTooltip="Report bug"
- (click)="goToLink('https://b.corp.google.com/issues/new?component=909476')">
- <mat-icon> bug_report</mat-icon>
- </button>
-
- <button
- mat-icon-button
- matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode"
- (click)="setDarkMode(!isDarkModeOn)">
- <mat-icon>
- {{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }}
- </mat-icon>
- </button>
+ <button
+ mat-icon-button
+ matTooltip="Switch to {{ isDarkModeOn ? 'light' : 'dark' }} mode"
+ (click)="setDarkMode(!isDarkModeOn)">
+ <mat-icon>
+ {{ isDarkModeOn ? 'brightness_5' : 'brightness_4' }}
+ </mat-icon>
+ </button>
+ </div>
</mat-toolbar>
<mat-divider></mat-divider>
- <mat-drawer-container class="example-container" autosize disableClose autoFocus>
+ <mat-drawer-container autosize disableClose autoFocus>
<mat-drawer-content>
<ng-container *ngIf="dataLoaded; else noLoadedTracesBlock">
- <trace-view
- class="viewers"
- [viewers]="viewers"
- [store]="store"
- (downloadTracesButtonClick)="onDownloadTracesButtonClick()"
- (activeViewChanged)="onActiveViewChanged($event)"></trace-view>
+ <trace-view class="viewers" [viewers]="viewers" [store]="store"></trace-view>
<mat-divider></mat-divider>
</ng-container>
@@ -134,14 +182,14 @@
<div class="card-grid landing-grid">
<collect-traces
class="collect-traces-card homepage-card"
- (filesCollected)="mediator.onWinscopeFilesCollected($event)"
+ (filesCollected)="onFilesCollected($event)"
[store]="store"></collect-traces>
<upload-traces
class="upload-traces-card homepage-card"
[tracePipeline]="tracePipeline"
- (filesUploaded)="mediator.onWinscopeFilesUploaded($event)"
- (viewTracesButtonClick)="mediator.onWinscopeViewTracesRequest()"></upload-traces>
+ (filesUploaded)="onFilesUploaded($event)"
+ (viewTracesButtonClick)="onViewTracesButtonClick()"></upload-traces>
</div>
</div>
</div>
@@ -151,6 +199,7 @@
`
.toolbar {
gap: 10px;
+ justify-content: space-between;
}
.welcome-info {
margin: 16px 0 6px 0;
@@ -163,13 +212,38 @@
overflow: auto;
height: 820px;
}
- .spacer {
- flex: 1;
- text-align: center;
- display: flex;
- align-items: center;
+ .file-descriptor {
+ font-size: 14px;
+ padding-left: 10px;
+ width: 350px;
+ }
+ .horizontal-align {
justify-content: center;
}
+ .vertical-align {
+ text-align: center;
+ align-items: center;
+ overflow-x: hidden;
+ display: flex;
+ }
+ .download-file-info {
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ padding-top: 3px;
+ max-width: 300px;
+ }
+ .file-name-input-field .right-align {
+ text-align: right;
+ }
+ .file-name-input-field .mat-form-field-wrapper {
+ padding-bottom: 10px;
+ width: 300px;
+ }
+ .trace-file-info {
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ max-width: 100%;
+ }
.viewers {
height: 0;
flex-grow: 1;
@@ -198,37 +272,51 @@
],
encapsulation: ViewEncapsulation.None,
})
-export class AppComponent implements TraceDataListener {
+export class AppComponent implements WinscopeEventListener {
title = 'winscope';
- changeDetectorRef: ChangeDetectorRef;
- snackbarOpener: SnackBarOpener;
- tracePipeline = new TracePipeline();
timelineData = new TimelineData();
abtChromeExtensionProtocol = new AbtChromeExtensionProtocol();
crossToolProtocol = new CrossToolProtocol();
- mediator: Mediator;
states = ProxyState;
- store: PersistentStore = new PersistentStore();
- currentTimestamp?: Timestamp;
- viewers: Viewer[] = [];
- isDarkModeOn!: boolean;
dataLoaded = false;
- activeView?: View;
- activeTrace?: Trace<object>;
+ showDataLoadedElements = false;
activeTraceFileInfo = '';
collapsedTimelineHeight = 0;
+ TRACE_INFO = TRACE_INFO;
+ isEditingFilename = false;
+ store: PersistentStore = new PersistentStore();
+ viewers: Viewer[] = [];
+
+ isDarkModeOn!: boolean;
+ changeDetectorRef: ChangeDetectorRef;
+ snackbarOpener: SnackBarOpener;
+ tracePipeline: TracePipeline;
+ mediator: Mediator;
+ currentTimestamp?: Timestamp;
+ activeView?: View;
+ activeTrace?: Trace<object>;
+ filenameFormControl: FormControl = new FormControl(
+ 'winscope',
+ Validators.compose([Validators.required, Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX)])
+ );
+
@ViewChild(UploadTracesComponent) uploadTracesComponent?: UploadTracesComponent;
@ViewChild(CollectTracesComponent) collectTracesComponent?: UploadTracesComponent;
+ @ViewChild(TraceViewComponent) traceViewComponent?: TraceViewComponent;
@ViewChild(TimelineComponent) timelineComponent?: TimelineComponent;
- TRACE_INFO = TRACE_INFO;
constructor(
@Inject(Injector) injector: Injector,
@Inject(ChangeDetectorRef) changeDetectorRef: ChangeDetectorRef,
- @Inject(SnackBarOpener) snackBar: SnackBarOpener
+ @Inject(SnackBarOpener) snackBar: SnackBarOpener,
+ @Inject(Title) private pageTitle: Title,
+ @Inject(NgZone) private ngZone: NgZone
) {
+ CrossPlatform.setCache(new NoCache());
+
this.changeDetectorRef = changeDetectorRef;
this.snackbarOpener = snackBar;
+ this.tracePipeline = new TracePipeline();
this.mediator = new Mediator(
this.tracePipeline,
this.timelineData,
@@ -293,13 +381,14 @@
}
}
- ngAfterViewInit() {
- this.mediator.onWinscopeInitialized();
+ async ngAfterViewInit() {
+ await this.mediator.onWinscopeEvent(new AppInitialized());
}
ngAfterViewChecked() {
this.mediator.setUploadTracesComponent(this.uploadTracesComponent);
this.mediator.setCollectTracesComponent(this.collectTracesComponent);
+ this.mediator.setTraceViewComponent(this.traceViewComponent);
this.mediator.setTimelineComponent(this.timelineComponent);
}
@@ -312,44 +401,93 @@
return this.tracePipeline.getTraces().mapTrace((trace) => trace.type);
}
- onTraceDataLoaded(viewers: Viewer[]) {
- this.viewers = viewers;
- this.dataLoaded = true;
- this.changeDetectorRef.detectChanges();
- }
-
- onTraceDataUnloaded() {
- proxyClient.adbData = [];
- this.dataLoaded = false;
- this.changeDetectorRef.detectChanges();
- }
-
setDarkMode(enabled: boolean) {
document.body.classList.toggle('dark-mode', enabled);
this.store.add('dark-mode', `${enabled}`);
this.isDarkModeOn = enabled;
}
+ onPencilIconClick() {
+ this.isEditingFilename = true;
+ }
+
+ onCheckIconClick() {
+ if (this.filenameFormControl.invalid) {
+ return;
+ }
+ this.isEditingFilename = false;
+ this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`);
+ }
+
async onDownloadTracesButtonClick() {
- const traceFiles = await this.makeTraceFilesForDownload();
- const zipFileBlob = await FileUtils.createZipArchive(traceFiles);
- const zipFileName = 'winscope.zip';
+ if (this.filenameFormControl.invalid) {
+ return;
+ }
+ await this.downloadTraces();
+ }
+
+ async onFilesCollected(files: File[]) {
+ await this.mediator.onWinscopeEvent(new AppFilesCollected(files));
+ }
+
+ async onFilesUploaded(files: File[]) {
+ await this.mediator.onWinscopeEvent(new AppFilesUploaded(files));
+ }
+
+ async onUploadNewButtonClick() {
+ await this.mediator.onWinscopeEvent(new AppResetRequest());
+ }
+
+ async onViewTracesButtonClick() {
+ await this.mediator.onWinscopeEvent(new AppTraceViewRequest());
+ }
+
+ async downloadTraces() {
+ const archiveBlob = await this.tracePipeline.makeZipArchiveWithLoadedTraceFiles();
+ const archiveFilename = `${this.filenameFormControl.value}.zip`;
const a = document.createElement('a');
document.body.appendChild(a);
- const url = window.URL.createObjectURL(zipFileBlob);
+ const url = window.URL.createObjectURL(archiveBlob);
a.href = url;
- a.download = zipFileName;
+ a.download = archiveFilename;
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
- async onActiveViewChanged(view: View) {
- this.activeView = view;
- this.activeTrace = this.getActiveTrace(view);
- this.activeTraceFileInfo = this.makeActiveTraceFileInfo(view);
- await this.mediator.onWinscopeActiveViewChanged(view);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TABBED_VIEW_SWITCHED, async (event) => {
+ this.activeView = event.newFocusedView;
+ this.activeTrace = this.getActiveTrace(event.newFocusedView);
+ this.activeTraceFileInfo = this.makeActiveTraceFileInfo(event.newFocusedView);
+ });
+
+ await event.visit(WinscopeEventType.VIEWERS_LOADED, async (event) => {
+ this.viewers = event.viewers;
+ this.filenameFormControl.setValue(this.tracePipeline.getDownloadArchiveFilename());
+ this.pageTitle.setTitle(`Winscope | ${this.filenameFormControl.value}`);
+ this.isEditingFilename = false;
+
+ // some elements e.g. timeline require dataLoaded to be set outside NgZone to render
+ this.dataLoaded = true;
+ this.changeDetectorRef.detectChanges();
+
+ // tooltips must be rendered inside ngZone due to limitation of MatTooltip,
+ // therefore toolbar elements controlled by a different boolean
+ this.ngZone.run(() => {
+ this.showDataLoadedElements = true;
+ });
+ });
+
+ await event.visit(WinscopeEventType.VIEWERS_UNLOADED, async (event) => {
+ proxyClient.adbData = [];
+ this.dataLoaded = false;
+ this.showDataLoadedElements = false;
+ this.pageTitle.setTitle('Winscope');
+ this.activeView = undefined;
+ this.changeDetectorRef.detectChanges();
+ });
}
goToLink(url: string) {
@@ -375,15 +513,4 @@
});
return activeTrace;
}
-
- private async makeTraceFilesForDownload(): Promise<File[]> {
- const loadedFiles = this.tracePipeline.getLoadedFiles();
- return [...loadedFiles.keys()].map((traceType) => {
- const file = loadedFiles.get(traceType)!;
- const path = TRACE_INFO[traceType].downloadArchiveDir;
-
- const newName = path + '/' + FileUtils.removeDirFromFileName(file.file.name);
- return new File([file.file], newName);
- });
- }
}
diff --git a/tools/winscope/src/app/components/app_component_stub.ts b/tools/winscope/src/app/components/app_component_stub.ts
deleted file mode 100644
index 2e84e8d..0000000
--- a/tools/winscope/src/app/components/app_component_stub.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.
- */
-
-import {TraceDataListener} from 'interfaces/trace_data_listener';
-import {Viewer} from 'viewers/viewer';
-
-export class AppComponentStub implements TraceDataListener {
- onTraceDataLoaded(viewers: Viewer[]) {
- // do nothing
- }
-
- onTraceDataUnloaded() {
- // do nothing
- }
-}
diff --git a/tools/winscope/src/app/components/app_component_test.ts b/tools/winscope/src/app/components/app_component_test.ts
index 937bf1c..0f8fafc 100644
--- a/tools/winscope/src/app/components/app_component_test.ts
+++ b/tools/winscope/src/app/components/app_component_test.ts
@@ -16,24 +16,30 @@
import {CommonModule} from '@angular/common';
import {ChangeDetectionStrategy} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
-import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatSliderModule} from '@angular/material/slider';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
+import {Title} from '@angular/platform-browser';
+import {FileUtils} from 'common/file_utils';
+import {ViewersLoaded, ViewersUnloaded} from 'messaging/winscope_event';
import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
import {AdbProxyComponent} from './adb_proxy_component';
import {AppComponent} from './app_component';
import {MatDrawer, MatDrawerContainer, MatDrawerContent} from './bottomnav/bottom_drawer_component';
import {CollectTracesComponent} from './collect_traces_component';
-import {MiniTimelineComponent} from './timeline/mini_timeline_component';
+import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component';
import {TimelineComponent} from './timeline/timeline_component';
import {TraceConfigComponent} from './trace_config_component';
import {TraceViewComponent} from './trace_view_component';
@@ -47,7 +53,7 @@
beforeEach(async () => {
await TestBed.configureTestingModule({
- providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
+ providers: [Title, {provide: ComponentFixtureAutoDetect, useValue: true}],
imports: [
CommonModule,
FormsModule,
@@ -62,6 +68,8 @@
MatToolbarModule,
MatTooltipModule,
ReactiveFormsModule,
+ MatInputModule,
+ BrowserAnimationsModule,
],
declarations: [
AdbProxyComponent,
@@ -86,6 +94,14 @@
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
+ component.filenameFormControl = new FormControl(
+ 'winscope',
+ Validators.compose([
+ Validators.required,
+ Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX),
+ ])
+ );
+ fixture.detectChanges();
});
it('can be created', () => {
@@ -97,14 +113,18 @@
});
it('renders the page title', () => {
- expect(htmlElement.querySelector('.app-title')?.innerHTML).toContain('Winscope');
+ const title = assertDefined(htmlElement.querySelector('.app-title'));
+ expect(title.innerHTML).toContain('Winscope');
});
it('displays correct elements when no data loaded', () => {
component.dataLoaded = false;
+ component.showDataLoadedElements = false;
fixture.detectChanges();
+
expect(htmlElement.querySelector('.welcome-info')).toBeTruthy();
- expect(htmlElement.querySelector('.active-trace-file-info')).toBeFalsy();
+ expect(htmlElement.querySelector('.trace-file-info')).toBeFalsy();
+ expect(htmlElement.querySelector('.active')).toBeFalsy();
expect(htmlElement.querySelector('.collect-traces-card')).toBeTruthy();
expect(htmlElement.querySelector('.upload-traces-card')).toBeTruthy();
expect(htmlElement.querySelector('.viewers')).toBeFalsy();
@@ -112,11 +132,130 @@
it('displays correct elements when data loaded', () => {
component.dataLoaded = true;
+ component.showDataLoadedElements = true;
fixture.detectChanges();
+
expect(htmlElement.querySelector('.welcome-info')).toBeFalsy();
- expect(htmlElement.querySelector('.active-trace-file-info')).toBeTruthy();
+ expect(htmlElement.querySelector('.trace-file-info')).toBeTruthy();
+ expect(htmlElement.querySelector('.active')).toBeTruthy();
+ expect(htmlElement.querySelector('.save-button')).toBeTruthy();
expect(htmlElement.querySelector('.collect-traces-card')).toBeFalsy();
expect(htmlElement.querySelector('.upload-traces-card')).toBeFalsy();
expect(htmlElement.querySelector('.viewers')).toBeTruthy();
});
+
+ it('downloads traces on download button click', () => {
+ component.showDataLoadedElements = true;
+ fixture.detectChanges();
+ const spy = spyOn(component, 'downloadTraces');
+
+ clickDownloadTracesButton();
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ clickDownloadTracesButton();
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('downloads traces after valid file name change', () => {
+ component.showDataLoadedElements = true;
+ fixture.detectChanges();
+ const spy = spyOn(component, 'downloadTraces');
+
+ clickEditFilenameButton();
+ updateFilenameInputAndDownloadTraces('Winscope2', true);
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ // check it works twice in a row
+ clickEditFilenameButton();
+ updateFilenameInputAndDownloadTraces('win_scope', true);
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('changes page title based on archive name', async () => {
+ const pageTitle = TestBed.inject(Title);
+
+ await component.onWinscopeEvent(new ViewersUnloaded());
+ expect(pageTitle.getTitle()).toBe('Winscope');
+
+ component.tracePipeline.getDownloadArchiveFilename = jasmine
+ .createSpy()
+ .and.returnValue('test_archive');
+ await component.onWinscopeEvent(new ViewersLoaded([]));
+ fixture.detectChanges();
+ expect(pageTitle.getTitle()).toBe('Winscope | test_archive');
+ });
+
+ it('does not download traces if invalid file name chosen', () => {
+ component.showDataLoadedElements = true;
+ fixture.detectChanges();
+ const spy = spyOn(component, 'downloadTraces');
+
+ clickEditFilenameButton();
+ updateFilenameInputAndDownloadTraces('w?n$cope', false);
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('behaves as expected when entering valid then invalid then valid file names', () => {
+ component.showDataLoadedElements = true;
+ fixture.detectChanges();
+
+ const spy = spyOn(component, 'downloadTraces');
+
+ clickEditFilenameButton();
+ updateFilenameInputAndDownloadTraces('Winscope2', true);
+ expect(spy).toHaveBeenCalled();
+
+ clickEditFilenameButton();
+ updateFilenameInputAndDownloadTraces('w?n$cope', false);
+ expect(spy).toHaveBeenCalledTimes(1);
+
+ updateFilenameInputAndDownloadTraces('win.scope', true);
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('validates filename on enter key', () => {
+ const spy = spyOn(component, 'onCheckIconClick');
+
+ component.showDataLoadedElements = true;
+ fixture.detectChanges();
+
+ clickEditFilenameButton();
+
+ const inputField = assertDefined(htmlElement.querySelector('.file-name-input-field'));
+ const inputEl = assertDefined(htmlElement.querySelector('.file-name-input-field input'));
+ (inputEl as HTMLInputElement).value = 'valid_file_name';
+ inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
+
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ function updateFilenameInputAndDownloadTraces(name: string, valid: boolean) {
+ const inputEl = assertDefined(htmlElement.querySelector('.file-name-input-field input'));
+ const checkButton = assertDefined(htmlElement.querySelector('.check-button'));
+ (inputEl as HTMLInputElement).value = name;
+ inputEl.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ checkButton.dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ if (valid) {
+ expect(htmlElement.querySelector('.download-file-info')).toBeTruthy();
+ clickDownloadTracesButton();
+ } else {
+ expect(htmlElement.querySelector('.save-button')).toBeFalsy();
+ expect(htmlElement.querySelector('.download-file-info')).toBeFalsy();
+ }
+ }
+
+ function clickDownloadTracesButton() {
+ const downloadButton = assertDefined(htmlElement.querySelector('.save-button'));
+ downloadButton.dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ }
+
+ function clickEditFilenameButton() {
+ const pencilButton = assertDefined(htmlElement.querySelector('.edit-button'));
+ pencilButton.dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ }
});
diff --git a/tools/winscope/src/app/components/collect_traces_component.ts b/tools/winscope/src/app/components/collect_traces_component.ts
index edcc2e4..eb7ea27 100644
--- a/tools/winscope/src/app/components/collect_traces_component.ts
+++ b/tools/winscope/src/app/components/collect_traces_component.ts
@@ -27,7 +27,7 @@
ViewEncapsulation,
} from '@angular/core';
import {PersistentStore} from 'common/persistent_store';
-import {ProgressListener} from 'interfaces/progress_listener';
+import {ProgressListener} from 'messaging/progress_listener';
import {Connection} from 'trace_collection/connection';
import {ProxyState} from 'trace_collection/proxy_client';
import {ProxyConnection} from 'trace_collection/proxy_connection';
@@ -482,7 +482,7 @@
private requestedTraces() {
const tracesFromCollection: string[] = [];
const tracingConfig = this.tracingConfig.getTraceConfig();
- const req = Object.keys(tracingConfig).filter((traceKey: string) => {
+ const requested = Object.keys(tracingConfig).filter((traceKey: string) => {
const traceConfig = tracingConfig[traceKey];
if (traceConfig.isTraceCollection) {
traceConfig.config?.enableConfigs.forEach((innerTrace: EnableConfiguration) => {
@@ -494,14 +494,18 @@
}
return traceConfig.run;
});
- return req.concat(tracesFromCollection);
+ requested.push(...tracesFromCollection);
+ requested.push('perfetto_trace'); // always start/stop/fetch perfetto trace
+ return requested;
}
private requestedDumps() {
const dumpConfig = this.tracingConfig.getDumpConfig();
- return Object.keys(dumpConfig).filter((dumpKey: string) => {
+ const requested = Object.keys(dumpConfig).filter((dumpKey: string) => {
return dumpConfig[dumpKey].run;
});
+ requested.push('perfetto_dump'); // always dump/fetch perfetto dump
+ return requested;
}
private requestedEnableConfig(): string[] {
diff --git a/tools/winscope/src/app/components/snack_bar_opener.ts b/tools/winscope/src/app/components/snack_bar_opener.ts
index d9c35c6..3c14ca5 100644
--- a/tools/winscope/src/app/components/snack_bar_opener.ts
+++ b/tools/winscope/src/app/components/snack_bar_opener.ts
@@ -17,8 +17,8 @@
import {Inject, Injectable, NgZone} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';
import {TRACE_INFO} from 'app/trace_info';
-import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {ParserError, ParserErrorType} from 'parsers/parser_factory';
+import {UserNotificationListener} from 'messaging/user_notification_listener';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
import {SnackBarComponent} from './snack_bar_component';
@Injectable({providedIn: 'root'})
@@ -28,7 +28,7 @@
@Inject(MatSnackBar) private snackBar: MatSnackBar
) {}
- onParserErrors(errors: ParserError[]) {
+ onErrors(errors: WinscopeError[]) {
const messages = this.convertErrorsToMessages(errors);
if (messages.length === 0) {
@@ -45,7 +45,7 @@
});
}
- private convertErrorsToMessages(errors: ParserError[]): string[] {
+ private convertErrorsToMessages(errors: WinscopeError[]): string[] {
const messages: string[] = [];
const groups = this.groupErrorsByType(errors);
@@ -66,37 +66,39 @@
return messages;
}
- private convertErrorToMessage(error: ParserError): string {
+ private convertErrorToMessage(error: WinscopeError): string {
const fileName = error.trace !== undefined ? error.trace : '<no file name>';
- const traceTypeName =
- error.traceType !== undefined ? TRACE_INFO[error.traceType].name : '<unknown>';
+ const traceTypeInfo =
+ error.traceType !== undefined ? ` of type ${TRACE_INFO[error.traceType].name}` : '';
switch (error.type) {
- case ParserErrorType.NO_INPUT_FILES:
- return 'No input files';
- case ParserErrorType.UNSUPPORTED_FORMAT:
+ case WinscopeErrorType.CORRUPTED_ARCHIVE:
+ return `${fileName}: corrupted archive`;
+ case WinscopeErrorType.NO_INPUT_FILES:
+ return `Input doesn't contain trace files`;
+ case WinscopeErrorType.UNSUPPORTED_FILE_FORMAT:
return `${fileName}: unsupported file format`;
- case ParserErrorType.OVERRIDE: {
- return `${fileName}: overridden by another trace of type ${traceTypeName}`;
+ case WinscopeErrorType.FILE_OVERRIDDEN: {
+ return `${fileName}: overridden by another trace${traceTypeInfo}`;
}
default:
return `${fileName}: unknown error occurred`;
}
}
- private makeCroppedMessage(type: ParserErrorType, count: number): string {
+ private makeCroppedMessage(type: WinscopeErrorType, count: number): string {
switch (type) {
- case ParserErrorType.OVERRIDE:
+ case WinscopeErrorType.FILE_OVERRIDDEN:
return `... (cropped ${count} overridden trace messages)`;
- case ParserErrorType.UNSUPPORTED_FORMAT:
+ case WinscopeErrorType.UNSUPPORTED_FILE_FORMAT:
return `... (cropped ${count} unsupported file format messages)`;
default:
return `... (cropped ${count} unknown error messages)`;
}
}
- private groupErrorsByType(errors: ParserError[]): Map<ParserErrorType, ParserError[]> {
- const groups = new Map<ParserErrorType, ParserError[]>();
+ private groupErrorsByType(errors: WinscopeError[]): Map<WinscopeErrorType, WinscopeError[]> {
+ const groups = new Map<WinscopeErrorType, WinscopeError[]>();
errors.forEach((error) => {
if (groups.get(error.type) === undefined) {
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
new file mode 100644
index 0000000..dd9ab56
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/abstract_timeline_row_component.ts
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {ElementRef, EventEmitter, SimpleChanges} from '@angular/core';
+import {Point} from 'common/geometry_utils';
+import {TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {CanvasDrawer} from './canvas_drawer';
+
+export abstract class AbstractTimelineRowComponent<T extends {}> {
+ abstract selectedEntry: TraceEntry<T> | undefined;
+ abstract onTracePositionUpdate: EventEmitter<TracePosition>;
+ abstract wrapperRef: ElementRef;
+ abstract canvasRef: ElementRef;
+
+ canvasDrawer: CanvasDrawer = new CanvasDrawer();
+
+ getCanvas(): HTMLCanvasElement {
+ return this.canvasRef.nativeElement;
+ }
+
+ private _observer = new ResizeObserver(() => this.initializeCanvas());
+ async ngAfterViewInit() {
+ this._observer.observe(this.wrapperRef.nativeElement);
+ await this.initializeCanvas();
+ }
+
+ ngOnDestroy() {
+ this._observer.disconnect();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (this.viewInitialized) {
+ this.redraw();
+ }
+ }
+
+ protected viewInitialized = false;
+
+ async initializeCanvas() {
+ const canvas = this.getCanvas();
+
+ // Reset any size before computing new size to avoid it interfering with size computations
+ canvas.width = 0;
+ canvas.height = 0;
+ canvas.style.width = 'auto';
+ canvas.style.height = 'auto';
+
+ const computedStyle = getComputedStyle(this.wrapperRef.nativeElement);
+ const width = this.wrapperRef.nativeElement.offsetWidth;
+ const height =
+ this.wrapperRef.nativeElement.offsetHeight -
+ // tslint:disable-next-line:ban
+ parseFloat(computedStyle.paddingTop) -
+ // tslint:disable-next-line:ban
+ parseFloat(computedStyle.paddingBottom);
+
+ const HiPPIwidth = window.devicePixelRatio * width;
+ const HiPPIheight = window.devicePixelRatio * height;
+
+ canvas.width = HiPPIwidth;
+ canvas.height = HiPPIheight;
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+
+ // ensure all drawing operations are scaled
+ if (window.devicePixelRatio !== 1) {
+ const context = canvas.getContext('2d')!;
+ context.scale(window.devicePixelRatio, window.devicePixelRatio);
+ }
+
+ this.canvasDrawer.setCanvas(this.getCanvas());
+ await this.redraw();
+
+ canvas.addEventListener('mousemove', (event: MouseEvent) => {
+ this.handleMouseMove(event);
+ });
+ canvas.addEventListener('mousedown', (event: MouseEvent) => {
+ this.handleMouseDown(event);
+ });
+ canvas.addEventListener('mouseout', (event: MouseEvent) => {
+ this.handleMouseOut(event);
+ });
+
+ this.viewInitialized = true;
+ }
+
+ async handleMouseDown(e: MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ const mousePoint = {
+ x: e.offsetX,
+ y: e.offsetY,
+ };
+
+ const transitionEntry = await this.getEntryAt(mousePoint);
+ // TODO: This can probably get made better by getting the transition and checking both the end and start timestamps match
+ if (transitionEntry && transitionEntry !== this.selectedEntry) {
+ this.redraw();
+ this.selectedEntry = transitionEntry;
+ this.onTracePositionUpdate.emit(TracePosition.fromTraceEntry(transitionEntry));
+ }
+ }
+
+ handleMouseMove(e: MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ const mousePoint = {
+ x: e.offsetX,
+ y: e.offsetY,
+ };
+
+ this.updateCursor(mousePoint);
+ this.onHover(mousePoint);
+ }
+
+ protected async updateCursor(mousePoint: Point) {
+ if (this.getEntryAt(mousePoint) !== undefined) {
+ this.getCanvas().style.cursor = 'pointer';
+ } else {
+ this.getCanvas().style.cursor = 'auto';
+ }
+ }
+
+ protected abstract getEntryAt(mousePoint: Point): Promise<TraceEntry<T> | undefined>;
+ protected abstract onHover(mousePoint: Point): void;
+ protected abstract handleMouseOut(e: MouseEvent): void;
+
+ protected async redraw() {
+ this.canvasDrawer.clear();
+ await this.drawTimeline();
+ }
+
+ abstract drawTimeline(): Promise<void>;
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
new file mode 100644
index 0000000..2190fca
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {Rect} from 'common/geometry_utils';
+
+export class CanvasDrawer {
+ private canvas!: HTMLCanvasElement;
+ private ctx!: CanvasRenderingContext2D;
+
+ setCanvas(canvas: HTMLCanvasElement) {
+ this.canvas = canvas;
+ const ctx = this.canvas.getContext('2d');
+ if (ctx === null) {
+ throw new Error("Couldn't get context from canvas");
+ }
+ this.ctx = ctx;
+ }
+
+ drawRect(rect: Rect, color: string, alpha: number) {
+ const rgbColor = this.hexToRgb(color);
+ if (rgbColor === undefined) {
+ throw new Error('Failed to parse provided hex color');
+ }
+ const {r, g, b} = rgbColor;
+
+ this.defineRectPath(rect);
+ this.ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
+ this.ctx.fill();
+
+ this.ctx.restore();
+ }
+
+ drawRectBorder(rect: Rect) {
+ this.defineRectPath(rect);
+ this.highlightPath();
+ this.ctx.restore();
+ }
+
+ clear() {
+ this.ctx.clearRect(0, 0, this.getScaledCanvasWidth(), this.getScaledCanvasHeight());
+ }
+
+ getScaledCanvasWidth() {
+ return Math.floor(this.canvas.width / this.getXScale());
+ }
+
+ getScaledCanvasHeight() {
+ return Math.floor(this.canvas.height / this.getYScale());
+ }
+
+ getXScale(): number {
+ return this.ctx.getTransform().m11;
+ }
+
+ getYScale(): number {
+ return this.ctx.getTransform().m22;
+ }
+
+ private highlightPath() {
+ this.ctx.globalAlpha = 1.0;
+ this.ctx.lineWidth = 2;
+ this.ctx.save();
+ this.ctx.clip();
+ this.ctx.lineWidth *= 2;
+ this.ctx.stroke();
+ this.ctx.restore();
+ this.ctx.stroke();
+ }
+
+ private defineRectPath(rect: Rect) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(rect.x, rect.y);
+ this.ctx.lineTo(rect.x + rect.w, rect.y);
+ this.ctx.lineTo(rect.x + rect.w, rect.y + rect.h);
+ this.ctx.lineTo(rect.x, rect.y + rect.h);
+ this.ctx.lineTo(rect.x, rect.y);
+ this.ctx.closePath();
+ }
+
+ private hexToRgb(hex: string): {r: number; g: number; b: number} | undefined {
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ hex = hex.replace(shorthandRegex, (m, r, g, b) => {
+ return r + r + g + g + b + b;
+ });
+
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ // tslint:disable-next-line:ban
+ r: parseInt(result[1], 16),
+ // tslint:disable-next-line:ban
+ g: parseInt(result[2], 16),
+ // tslint:disable-next-line:ban
+ b: parseInt(result[3], 16),
+ }
+ : undefined;
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
new file mode 100644
index 0000000..b759cec
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/canvas_drawer_test.ts
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {assertDefined} from 'common/assert_utils';
+import {CanvasDrawer} from './canvas_drawer';
+
+describe('CanvasDrawer', () => {
+ it('erases the canvas', async () => {
+ const actualCanvas = createCanvas(100, 100);
+ const expectedCanvas = createCanvas(100, 100);
+
+ const canvasDrawer = new CanvasDrawer();
+ canvasDrawer.setCanvas(actualCanvas);
+ canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 1.0);
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeFalse();
+
+ canvasDrawer.clear();
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+ });
+
+ it('can draw opaque rect', () => {
+ const actualCanvas = createCanvas(100, 100);
+ const expectedCanvas = createCanvas(100, 100);
+
+ const canvasDrawer = new CanvasDrawer();
+ canvasDrawer.setCanvas(actualCanvas);
+ canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 1.0);
+
+ const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+ expectedCtx.fillStyle = '#333333';
+ expectedCtx.rect(10, 10, 10, 10);
+ expectedCtx.fill();
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+ });
+
+ it('can draw translucent rect', () => {
+ const actualCanvas = createCanvas(100, 100);
+ const expectedCanvas = createCanvas(100, 100);
+
+ const canvasDrawer = new CanvasDrawer();
+ canvasDrawer.setCanvas(actualCanvas);
+ canvasDrawer.drawRect({x: 10, y: 10, w: 10, h: 10}, '#333333', 0.5);
+
+ const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+ expectedCtx.fillStyle = 'rgba(51,51,51,0.5)';
+ expectedCtx.rect(10, 10, 10, 10);
+ expectedCtx.fill();
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+ });
+
+ it('can draw rect border', () => {
+ const actualCanvas = createCanvas(100, 100);
+ const expectedCanvas = createCanvas(100, 100);
+
+ const canvasDrawer = new CanvasDrawer();
+ canvasDrawer.setCanvas(actualCanvas);
+ canvasDrawer.drawRectBorder({x: 10, y: 10, w: 10, h: 10});
+
+ const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+
+ expectedCtx.rect(9, 9, 12, 3);
+ expectedCtx.fill();
+ expectedCtx.rect(9, 9, 3, 12);
+ expectedCtx.fill();
+ expectedCtx.rect(9, 18, 12, 3);
+ expectedCtx.fill();
+ expectedCtx.rect(18, 9, 3, 12);
+ expectedCtx.fill();
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+ });
+
+ it('can draw rect outside bounds', () => {
+ const actualCanvas = createCanvas(100, 100);
+ const expectedCanvas = createCanvas(100, 100);
+
+ const canvasDrawer = new CanvasDrawer();
+ canvasDrawer.setCanvas(actualCanvas);
+ canvasDrawer.drawRect({x: 200, y: 200, w: 10, h: 10}, '#333333', 1.0);
+ canvasDrawer.drawRect({x: 95, y: 95, w: 50, h: 50}, '#333333', 1.0);
+
+ const expectedCtx = assertDefined(expectedCanvas.getContext('2d'));
+ expectedCtx.fillStyle = '#333333';
+ expectedCtx.rect(95, 95, 5, 5);
+ expectedCtx.fill();
+
+ expect(pixelsAllMatch(actualCanvas, expectedCanvas)).toBeTrue();
+ });
+});
+
+function createCanvas(width: number, height: number): HTMLCanvasElement {
+ const canvas = document.createElement('canvas') as HTMLCanvasElement;
+ canvas.width = width;
+ canvas.height = height;
+ return canvas;
+}
+
+function pixelsAllMatch(canvasA: HTMLCanvasElement, canvasB: HTMLCanvasElement): boolean {
+ if (canvasA.width !== canvasB.width || canvasA.height !== canvasB.height) {
+ return false;
+ }
+
+ const imgA = assertDefined(canvasA.getContext('2d')).getImageData(
+ 0,
+ 0,
+ canvasA.width,
+ canvasA.height
+ ).data;
+ const imgB = assertDefined(canvasB.getContext('2d')).getImageData(
+ 0,
+ 0,
+ canvasB.width,
+ canvasB.height
+ ).data;
+
+ for (let i = 0; i < imgA.length; i++) {
+ if (imgA[i] !== imgB[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
new file mode 100644
index 0000000..1168081
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {GeometryUtils, Point, Rect} from 'common/geometry_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {Trace, TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+
+@Component({
+ selector: 'single-timeline',
+ template: `
+ <div class="single-timeline" #wrapper>
+ <canvas #canvas></canvas>
+ </div>
+ `,
+ styles: [
+ `
+ .single-timeline {
+ height: 2rem;
+ padding: 1rem 0;
+ }
+ `,
+ ],
+})
+export class DefaultTimelineRowComponent extends AbstractTimelineRowComponent<{}> {
+ @Input() color = '#AF5CF7';
+ @Input() trace!: Trace<{}>;
+ @Input() selectedEntry: TraceEntry<{}> | undefined = undefined;
+ @Input() selectionRange!: TimeRange;
+
+ @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+
+ @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
+ @ViewChild('wrapper', {static: false}) wrapperRef!: ElementRef;
+
+ hoveringEntry?: Timestamp;
+ hoveringSegment?: TimeRange;
+
+ ngOnInit() {
+ if (!this.trace || !this.selectionRange) {
+ throw Error('Not all required inputs have been set');
+ }
+ }
+
+ override onHover(mousePoint: Point) {
+ this.drawEntryHover(mousePoint);
+ }
+
+ override handleMouseOut(e: MouseEvent) {
+ if (this.hoveringEntry || this.hoveringSegment) {
+ // If undefined there is no current hover effect so no need to clear
+ this.redraw();
+ }
+ this.hoveringEntry = undefined;
+ this.hoveringSegment = undefined;
+ }
+
+ private async drawEntryHover(mousePoint: Point) {
+ const currentHoverEntry = (await this.getEntryAt(mousePoint))?.getTimestamp();
+
+ if (this.hoveringEntry === currentHoverEntry) {
+ return;
+ }
+
+ if (this.hoveringEntry) {
+ // If null there is no current hover effect so no need to clear
+ this.canvasDrawer.clear();
+ this.drawTimeline();
+ }
+
+ this.hoveringEntry = currentHoverEntry;
+
+ if (!this.hoveringEntry) {
+ return;
+ }
+
+ const rect = this.entryRect(this.hoveringEntry);
+
+ this.canvasDrawer.drawRect(rect, this.color, 1.0);
+ this.canvasDrawer.drawRectBorder(rect);
+ }
+
+ protected override async getEntryAt(mousePoint: Point): Promise<TraceEntry<{}> | undefined> {
+ const timestampOfClick = this.getTimestampOf(mousePoint.x);
+ const candidateEntry = this.trace.findLastLowerOrEqualEntry(timestampOfClick);
+
+ if (candidateEntry !== undefined) {
+ const timestamp = candidateEntry.getTimestamp();
+ const rect = this.entryRect(timestamp);
+ if (GeometryUtils.isPointInRect(mousePoint, rect)) {
+ return candidateEntry;
+ }
+ }
+
+ return undefined;
+ }
+
+ get entryWidth() {
+ return this.canvasDrawer.getScaledCanvasHeight();
+ }
+
+ get availableWidth() {
+ return Math.floor(this.canvasDrawer.getScaledCanvasWidth() - this.entryWidth);
+ }
+
+ private entryRect(entry: Timestamp, padding = 0): Rect {
+ const xPos = this.getXPosOf(entry);
+
+ return {
+ x: xPos + padding,
+ y: padding,
+ w: this.entryWidth - 2 * padding,
+ h: this.entryWidth - 2 * padding,
+ };
+ }
+
+ private getXPosOf(entry: Timestamp): number {
+ const start = this.selectionRange.from.getValueNs();
+ const end = this.selectionRange.to.getValueNs();
+
+ return Number((BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start));
+ }
+
+ private getTimestampOf(x: number): Timestamp {
+ const start = this.selectionRange.from.getValueNs();
+ const end = this.selectionRange.to.getValueNs();
+ const ts = (BigInt(Math.floor(x)) * (end - start)) / BigInt(this.availableWidth) + start;
+ return new Timestamp(this.selectionRange.from.getType(), ts);
+ }
+
+ override async drawTimeline() {
+ this.trace
+ .sliceTime(this.selectionRange.from, this.selectionRange.to)
+ .forEachTimestamp((entry) => {
+ this.drawEntry(entry);
+ });
+ this.drawSelectedEntry();
+ }
+
+ private drawEntry(entry: Timestamp) {
+ const rect = this.entryRect(entry);
+
+ this.canvasDrawer.drawRect(rect, this.color, 0.2);
+ }
+
+ private drawSelectedEntry() {
+ if (this.selectedEntry === undefined) {
+ return;
+ }
+
+ const rect = this.entryRect(this.selectedEntry.getTimestamp(), 1);
+ this.canvasDrawer.drawRect(rect, this.color, 1.0);
+ this.canvasDrawer.drawRectBorder(rect);
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
new file mode 100644
index 0000000..6b3fd40
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
@@ -0,0 +1,305 @@
+/*
+ * 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.
+ */
+
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {waitToBeCalled} from 'test/utils';
+import {TraceType} from 'trace/trace_type';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+
+describe('DefaultTimelineRowComponent', () => {
+ let fixture: ComponentFixture<DefaultTimelineRowComponent>;
+ let component: DefaultTimelineRowComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ DragDropModule,
+ ],
+ declarations: [DefaultTimelineRowComponent],
+ })
+ .overrideComponent(DefaultTimelineRowComponent, {
+ set: {changeDetection: ChangeDetectionStrategy.Default},
+ })
+ .compileComponents();
+ fixture = TestBed.createComponent(DefaultTimelineRowComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('can draw entries', async () => {
+ setTraceAndSelectionRange(10n, 110n);
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect').and.callThrough();
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ drawRectSpy.calls.reset();
+ await waitToBeCalled(drawRectSpy, 4);
+
+ const width = 32;
+ const height = width;
+ const alpha = 0.2;
+
+ const canvasWidth = component.canvasDrawer.getScaledCanvasWidth() - width;
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(4);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: 0,
+ y: 0,
+ w: width,
+ h: height,
+ },
+ component.color,
+ alpha
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor((canvasWidth * 2) / 100),
+ y: 0,
+ w: width,
+ h: height,
+ },
+ component.color,
+ alpha
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor((canvasWidth * 5) / 100),
+ y: 0,
+ w: width,
+ h: height,
+ },
+ component.color,
+ alpha
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor((canvasWidth * 60) / 100),
+ y: 0,
+ w: width,
+ h: height,
+ },
+ component.color,
+ alpha
+ );
+ });
+
+ it('can draw entries zoomed in', async () => {
+ setTraceAndSelectionRange(60n, 85n);
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ drawRectSpy.calls.reset();
+ await waitToBeCalled(drawRectSpy, 1);
+
+ const width = 32;
+ const height = width;
+ const alpha = 0.2;
+
+ const canvasWidth = component.canvasDrawer.getScaledCanvasWidth() - width;
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor((canvasWidth * 10) / 25),
+ y: 0,
+ w: width,
+ h: height,
+ },
+ component.color,
+ alpha
+ );
+ });
+
+ it('can draw hovering entry', async () => {
+ setTraceAndSelectionRange(10n, 110n);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect').and.callThrough();
+ const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder').and.callThrough();
+
+ const waitPromises = [waitToBeCalled(drawRectBorderSpy, 1), waitToBeCalled(drawRectSpy, 1)];
+
+ component.handleMouseMove({
+ offsetX: 5,
+ offsetY: component.canvasDrawer.getScaledCanvasHeight() / 2,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ } as MouseEvent);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ await Promise.all(waitPromises);
+
+ expect(assertDefined(component.hoveringEntry).getValueNs()).toBe(10n);
+ expect(drawRectSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: 0,
+ y: 0,
+ w: 32,
+ h: 32,
+ },
+ component.color,
+ 1.0
+ );
+
+ expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectBorderSpy).toHaveBeenCalledWith({
+ x: 0,
+ y: 0,
+ w: 32,
+ h: 32,
+ });
+ });
+
+ it('can draw correct entry on click of first entry', async () => {
+ setTraceAndSelectionRange(10n, 110n);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ // 9 rect draws - 4 entry rects present + 4 for redraw + 1 for selected entry
+ await drawCorrectEntryOnClick(0, 10n, 9);
+ });
+
+ it('can draw correct entry on click of middle entry', async () => {
+ setTraceAndSelectionRange(10n, 110n);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ const canvasWidth = Math.floor(component.canvasDrawer.getScaledCanvasWidth() - 32);
+ const entryPos = Math.floor((canvasWidth * 5) / 100);
+
+ // 9 rect draws - 4 entry rects present + 4 for redraw + 1 for selected entry
+ await drawCorrectEntryOnClick(entryPos, 15n, 9);
+ });
+
+ it('can draw correct entry on click when timeline zoomed in near start', async () => {
+ setTraceAndSelectionRange(10n, 15n);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ const canvasWidth = Math.floor(component.canvasDrawer.getScaledCanvasWidth() - 32);
+ const entryPos = Math.floor((canvasWidth * 2) / 5);
+
+ // 5 rect draws - 2 entry rects present + 2 for redraw + 1 for selected entry
+ await drawCorrectEntryOnClick(entryPos, 12n, 5);
+ });
+
+ it('can draw correct entry on click when timeline zoomed in near end', async () => {
+ setTraceAndSelectionRange(60n, 80n);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ const canvasWidth = Math.floor(component.canvasDrawer.getScaledCanvasWidth() - 32);
+ const entryPos = Math.floor((canvasWidth * 10) / 20);
+
+ // 3 rect draws - 1 entry rects present + 1 for redraw + 1 for selected entry
+ await drawCorrectEntryOnClick(entryPos, 70n, 3);
+ });
+
+ function setTraceAndSelectionRange(low: bigint, high: bigint) {
+ component.trace = new TraceBuilder<{}>()
+ .setType(TraceType.TRANSITION)
+ .setEntries([{}, {}, {}, {}])
+ .setTimestamps([
+ new RealTimestamp(10n),
+ new RealTimestamp(12n),
+ new RealTimestamp(15n),
+ new RealTimestamp(70n),
+ ])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(low), to: new RealTimestamp(high)};
+ }
+
+ async function drawCorrectEntryOnClick(
+ xPos: number,
+ expectedTimestampNs: bigint,
+ rectSpyCalls: number
+ ) {
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect').and.callThrough();
+ const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder').and.callThrough();
+
+ const waitPromises = [
+ waitToBeCalled(drawRectSpy, rectSpyCalls),
+ waitToBeCalled(drawRectBorderSpy, 1),
+ ];
+
+ await component.handleMouseDown({
+ offsetX: xPos + 1,
+ offsetY: component.canvasDrawer.getScaledCanvasHeight() / 2,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ } as MouseEvent);
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ await Promise.all(waitPromises);
+
+ expect(assertDefined(component.selectedEntry).getTimestamp().getValueNs()).toBe(
+ expectedTimestampNs
+ );
+
+ const expectedRect = {
+ x: xPos + 1,
+ y: 1,
+ w: 30,
+ h: 30,
+ };
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(rectSpyCalls);
+ expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 1.0);
+
+ expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectBorderSpy).toHaveBeenCalledWith(expectedRect);
+ }
+});
diff --git a/tools/winscope/src/app/components/timeline/expanded_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
similarity index 68%
rename from tools/winscope/src/app/components/timeline/expanded_timeline_component.ts
rename to tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
index c392d85..63827ec 100644
--- a/tools/winscope/src/app/components/timeline/expanded_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
@@ -27,18 +27,20 @@
} from '@angular/core';
import {TimelineData} from 'app/timeline_data';
import {TRACE_INFO} from 'app/trace_info';
-import {Timestamp} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {TracePosition} from 'trace/trace_position';
-import {SingleTimelineComponent} from './single_timeline_component';
+import {TraceType, TraceTypeUtils} from 'trace/trace_type';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+import {TransitionTimelineComponent} from './transition_timeline_component';
@Component({
selector: 'expanded-timeline',
template: `
<div id="expanded-timeline-wrapper" #expandedTimelineWrapper>
<div
- *ngFor="let trace of this.timelineData.getTraces(); trackBy: trackTraceBySelectedTimestamp"
- class="timeline">
+ *ngFor="let trace of getTracesSortedByDisplayOrder(); trackBy: trackTraceByType"
+ class="timeline row">
<div class="icon-wrapper">
<mat-icon
class="icon"
@@ -47,13 +49,25 @@
{{ TRACE_INFO[trace.type].icon }}
</mat-icon>
</div>
- <single-timeline
+ <transition-timeline
+ *ngIf="trace.type === TraceType.TRANSITION"
[color]="TRACE_INFO[trace.type].color"
[trace]="trace"
[selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
[selectionRange]="timelineData.getSelectionTimeRange()"
(onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
- class="single-timeline"></single-timeline>
+ class="single-timeline">
+ </transition-timeline>
+ <single-timeline
+ *ngIf="trace.type !== TraceType.TRANSITION"
+ [color]="TRACE_INFO[trace.type].color"
+ [trace]="trace"
+ [selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
+ [selectionRange]="timelineData.getSelectionTimeRange()"
+ (onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
+ class="single-timeline">
+ </single-timeline>
+
<div class="icon-wrapper">
<mat-icon class="icon placeholder-icon"></mat-icon>
</div>
@@ -144,35 +158,50 @@
@ViewChild('canvas', {static: false}) canvasRef!: ElementRef<HTMLCanvasElement>;
@ViewChild('expandedTimelineWrapper', {static: false}) warpperRef!: ElementRef;
- @ViewChildren(SingleTimelineComponent) singleTimelines!: QueryList<SingleTimelineComponent>;
+
+ @ViewChildren(DefaultTimelineRowComponent)
+ singleTimelines: QueryList<DefaultTimelineRowComponent> | undefined;
+
+ @ViewChildren(TransitionTimelineComponent)
+ transitionTimelines: QueryList<TransitionTimelineComponent> | undefined;
TRACE_INFO = TRACE_INFO;
+ TraceType = TraceType;
@HostListener('window:resize', ['$event'])
onResize(event: Event) {
this.resizeCanvases();
}
- trackTraceBySelectedTimestamp = (index: number, trace: Trace<{}>): Timestamp | undefined => {
- return this.timelineData.findCurrentEntryFor(trace.type)?.getTimestamp();
+ trackTraceByType = (index: number, trace: Trace<{}>): TraceType => {
+ return trace.type;
};
+ getTracesSortedByDisplayOrder(): Array<Trace<{}>> {
+ const traces = this.timelineData.getTraces().mapTrace((trace) => trace);
+ return traces.sort((a, b) => TraceTypeUtils.compareByDisplayOrder(a.type, b.type));
+ }
+
private resizeCanvases() {
// Reset any size before computing new size to avoid it interfering with size computations.
// Needs to be done together because otherwise the sizes of each timeline will interfere with
// each other, since if one timeline is still too big the container will stretch to that size.
- for (const timeline of this.singleTimelines) {
- timeline.canvas.width = 0;
- timeline.canvas.height = 0;
- timeline.canvas.style.width = 'auto';
- timeline.canvas.style.height = 'auto';
+ const timelines = [
+ ...(this.transitionTimelines as QueryList<AbstractTimelineRowComponent<{}>>),
+ ...(this.singleTimelines as QueryList<AbstractTimelineRowComponent<{}>>),
+ ];
+ for (const timeline of timelines) {
+ timeline.getCanvas().width = 0;
+ timeline.getCanvas().height = 0;
+ timeline.getCanvas().style.width = 'auto';
+ timeline.getCanvas().style.height = 'auto';
}
- for (const timeline of this.singleTimelines) {
+ for (const timeline of timelines) {
timeline.initializeCanvas();
- timeline.canvas.height = 0;
- timeline.canvas.style.width = 'auto';
- timeline.canvas.style.height = 'auto';
+ timeline.getCanvas().height = 0;
+ timeline.getCanvas().style.width = 'auto';
+ timeline.getCanvas().style.height = 'auto';
}
}
}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
new file mode 100644
index 0000000..3bc3469
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TimelineData} from 'app/timeline_data';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {TracesBuilder} from 'test/unit/traces_builder';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {DefaultTimelineRowComponent} from './default_timeline_row_component';
+import {ExpandedTimelineComponent} from './expanded_timeline_component';
+import {TransitionTimelineComponent} from './transition_timeline_component';
+
+describe('ExpandedTimelineComponent', () => {
+ let fixture: ComponentFixture<ExpandedTimelineComponent>;
+ let component: ExpandedTimelineComponent;
+ let htmlElement: HTMLElement;
+ let timelineData: TimelineData;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ DragDropModule,
+ ],
+ declarations: [
+ ExpandedTimelineComponent,
+ TransitionTimelineComponent,
+ DefaultTimelineRowComponent,
+ ],
+ })
+ .overrideComponent(ExpandedTimelineComponent, {
+ set: {changeDetection: ChangeDetectionStrategy.Default},
+ })
+ .compileComponents();
+ fixture = TestBed.createComponent(ExpandedTimelineComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ timelineData = new TimelineData();
+ const traces = new TracesBuilder()
+ .setEntries(TraceType.SURFACE_FLINGER, [{}])
+ .setTimestamps(TraceType.SURFACE_FLINGER, [new RealTimestamp(10n)])
+ .setEntries(TraceType.WINDOW_MANAGER, [{}])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [new RealTimestamp(11n)])
+ .setEntries(TraceType.TRANSACTIONS, [{}])
+ .setTimestamps(TraceType.TRANSACTIONS, [new RealTimestamp(12n)])
+ .setEntries(TraceType.TRANSITION, [
+ {
+ createTime: {unixNanos: 10n},
+ finishTime: {unixNanos: 30n},
+ } as Transition,
+ {
+ createTime: {unixNanos: 60n},
+ finishTime: {unixNanos: 110n},
+ } as Transition,
+ ])
+ .setTimestamps(TraceType.TRANSITION, [new RealTimestamp(10n), new RealTimestamp(60n)])
+ .setTimestamps(TraceType.PROTO_LOG, [])
+ .build();
+ timelineData.initialize(traces, undefined);
+ component.timelineData = timelineData;
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('renders all timelines', () => {
+ fixture.detectChanges();
+
+ const timelineElements = htmlElement.querySelectorAll('.timeline.row single-timeline');
+ expect(timelineElements.length).toEqual(4);
+
+ const transitionElement = htmlElement.querySelectorAll('.timeline.row transition-timeline');
+ expect(transitionElement.length).toEqual(1);
+ });
+
+ it('passes initial selectedEntry of correct type into each timeline', () => {
+ fixture.detectChanges();
+
+ const singleTimelines = assertDefined(component.singleTimelines);
+ expect(singleTimelines.length).toBe(4);
+
+ // initially only first entry of SF is set
+ singleTimelines.forEach((timeline) => {
+ if (timeline.trace.type === TraceType.SURFACE_FLINGER) {
+ const entry = assertDefined(timeline.selectedEntry);
+ expect(entry.getFullTrace().type).toBe(TraceType.SURFACE_FLINGER);
+ } else {
+ expect(timeline.selectedEntry).toBeUndefined();
+ }
+ });
+
+ const transitionTimeline = assertDefined(component.transitionTimelines).first;
+ assertDefined(transitionTimeline.selectedEntry);
+ });
+
+ it('passes selectedEntry of correct type into each timeline on position change', () => {
+ // 3 out of the 5 traces have timestamps before or at 11n
+ component.timelineData.setPosition(TracePosition.fromTimestamp(new RealTimestamp(11n)));
+ fixture.detectChanges();
+
+ const singleTimelines = assertDefined(component.singleTimelines);
+ expect(singleTimelines.length).toBe(4);
+
+ singleTimelines.forEach((timeline) => {
+ // protolog and transactions traces have no timestamps before current position
+ if (
+ timeline.trace.type === TraceType.PROTO_LOG ||
+ timeline.trace.type === TraceType.TRANSACTIONS
+ ) {
+ expect(timeline.selectedEntry).toBeUndefined();
+ } else {
+ const selectedEntry = assertDefined(timeline.selectedEntry);
+ expect(selectedEntry.getFullTrace().type).toEqual(timeline.trace.type);
+ }
+ });
+
+ const transitionTimeline = assertDefined(component.transitionTimelines).first;
+ const selectedEntry = assertDefined(transitionTimeline.selectedEntry);
+ expect(selectedEntry.getFullTrace().type).toEqual(transitionTimeline.trace.type);
+ });
+
+ it('getAllLoadedTraces causes timelines to render in correct order', () => {
+ // traces in timelineData are in order of being set in Traces API
+ expect(component.timelineData.getTraces().mapTrace((trace) => trace.type)).toEqual([
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.TRANSACTIONS,
+ TraceType.TRANSITION,
+ TraceType.PROTO_LOG,
+ ]);
+
+ // getAllLoadedTraces returns traces in enum order
+ expect(component.getTracesSortedByDisplayOrder().map((trace) => trace.type)).toEqual([
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.TRANSACTIONS,
+ TraceType.PROTO_LOG,
+ TraceType.TRANSITION,
+ ]);
+ });
+});
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
new file mode 100644
index 0000000..836ab44
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
@@ -0,0 +1,269 @@
+/*
+ * 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.
+ */
+
+import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
+import {GeometryUtils, Point, Rect} from 'common/geometry_utils';
+import {ElapsedTimestamp, RealTimestamp, TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {Trace, TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
+
+@Component({
+ selector: 'transition-timeline',
+ template: `
+ <div
+ class="transition-timeline"
+ matTooltip="Some or all transitions will not be rendered in timeline due to unknown creation time"
+ [matTooltipDisabled]="shouldNotRenderEntries.length === 0"
+ #wrapper>
+ <canvas #canvas></canvas>
+ </div>
+ `,
+ styles: [
+ `
+ .transition-timeline {
+ height: 4rem;
+ }
+ `,
+ ],
+})
+export class TransitionTimelineComponent extends AbstractTimelineRowComponent<Transition> {
+ @Input() color = '#AF5CF7';
+ @Input() trace!: Trace<Transition>;
+ @Input() selectedEntry: TraceEntry<Transition> | undefined = undefined;
+ @Input() selectionRange!: TimeRange;
+
+ @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+
+ @ViewChild('canvas', {static: false}) override canvasRef!: ElementRef;
+ @ViewChild('wrapper', {static: false}) override wrapperRef!: ElementRef;
+
+ hoveringEntry?: TraceEntry<Transition>;
+
+ rowsToUse = new Map<number, number>();
+ maxRowsRequires = 0;
+ shouldNotRenderEntries: number[] = [];
+
+ ngOnInit() {
+ if (!this.trace || !this.selectionRange) {
+ throw Error('Not all required inputs have been set');
+ }
+ this.computeRowsForEntries();
+ }
+
+ private async computeRowsForEntries(): Promise<void> {
+ const rowAvailableFrom: Array<bigint | undefined> = [];
+ await Promise.all(
+ (this.trace as Trace<Transition>).mapEntry(async (entry) => {
+ const transition = await entry.getValue();
+ const index = entry.getIndex();
+ if (transition.createTime.isMin) {
+ this.shouldNotRenderEntries.push(index);
+ }
+ let rowToUse = 0;
+ while (
+ (rowAvailableFrom[rowToUse] ?? 0n) > BigInt(transition.createTime.unixNanos.toString())
+ ) {
+ rowToUse++;
+ }
+
+ rowAvailableFrom[rowToUse] = BigInt(transition.finishTime.unixNanos.toString());
+
+ if (rowToUse + 1 > this.maxRowsRequires) {
+ this.maxRowsRequires = rowToUse + 1;
+ }
+ this.rowsToUse.set(index, rowToUse);
+ })
+ );
+ }
+
+ private getRowToUseFor(entry: TraceEntry<Transition>): number {
+ const rowToUse = this.rowsToUse.get(entry.getIndex());
+ if (rowToUse === undefined) {
+ console.error('Failed to find', entry, 'in', this.rowsToUse);
+ throw new Error('Could not find entry in rowsToUse');
+ }
+ return rowToUse;
+ }
+
+ override onHover(mousePoint: Point) {
+ this.drawSegmentHover(mousePoint);
+ }
+
+ override handleMouseOut(e: MouseEvent) {
+ if (this.hoveringEntry) {
+ // If undefined there is no current hover effect so no need to clear
+ this.redraw();
+ }
+ this.hoveringEntry = undefined;
+ }
+
+ private async drawSegmentHover(mousePoint: Point) {
+ const currentHoverEntry = await this.getEntryAt(mousePoint);
+
+ if (this.hoveringEntry) {
+ this.canvasDrawer.clear();
+ this.drawTimeline();
+ }
+
+ this.hoveringEntry = currentHoverEntry;
+
+ if (!this.hoveringEntry || this.shouldNotRenderEntry(this.hoveringEntry)) {
+ return;
+ }
+
+ const hoveringSegment = await this.getSegmentForTransition(this.hoveringEntry);
+ const rowToUse = this.getRowToUseFor(this.hoveringEntry);
+ const rect = this.getSegmentRect(hoveringSegment.from, hoveringSegment.to, rowToUse);
+ this.canvasDrawer.drawRectBorder(rect);
+ }
+
+ protected override async getEntryAt(
+ mousePoint: Point
+ ): Promise<TraceEntry<Transition> | undefined> {
+ if (this.trace.type !== TraceType.TRANSITION) {
+ return undefined;
+ }
+
+ const transitionEntries: Array<Promise<TraceEntry<Transition> | undefined>> = [];
+ this.trace.forEachEntry((entry) => {
+ transitionEntries.push(
+ (async () => {
+ if (this.shouldNotRenderEntry(entry)) {
+ return undefined;
+ }
+ const transitionSegment = await this.getSegmentForTransition(entry);
+ const rowToUse = this.getRowToUseFor(entry);
+ const rect = this.getSegmentRect(transitionSegment.from, transitionSegment.to, rowToUse);
+ if (GeometryUtils.isPointInRect(mousePoint, rect)) {
+ return entry;
+ }
+ return undefined;
+ })()
+ );
+ });
+
+ for (const entryPromise of transitionEntries) {
+ const entry = await entryPromise;
+ if (entry) {
+ return entry;
+ }
+ }
+
+ return undefined;
+ }
+
+ get entryWidth() {
+ return this.canvasDrawer.getScaledCanvasHeight();
+ }
+
+ get availableWidth() {
+ return this.canvasDrawer.getScaledCanvasWidth();
+ }
+
+ private getXPosOf(entry: Timestamp): number {
+ const start = this.selectionRange.from.getValueNs();
+ const end = this.selectionRange.to.getValueNs();
+
+ return Number((BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start));
+ }
+
+ private getSegmentRect(start: Timestamp, end: Timestamp, rowToUse: number): Rect {
+ const xPosStart = this.getXPosOf(start);
+ const selectionStart = this.selectionRange.from.getValueNs();
+ const selectionEnd = this.selectionRange.to.getValueNs();
+
+ const width = Number(
+ (BigInt(this.availableWidth) * (end.getValueNs() - start.getValueNs())) /
+ (selectionEnd - selectionStart)
+ );
+
+ const borderPadding = 5;
+ let totalRowHeight =
+ (this.canvasDrawer.getScaledCanvasHeight() - 2 * borderPadding) / this.maxRowsRequires;
+ if (totalRowHeight < 10) {
+ totalRowHeight = 10;
+ }
+ if (this.maxRowsRequires === 1) {
+ totalRowHeight = 30;
+ }
+
+ const padding = 5;
+ const rowHeight = totalRowHeight - padding;
+
+ return {x: xPosStart, y: borderPadding + rowToUse * totalRowHeight, w: width, h: rowHeight};
+ }
+
+ override async drawTimeline() {
+ await Promise.all(
+ (this.trace as Trace<Transition>).mapEntry(async (entry) => {
+ if (this.shouldNotRenderEntry(entry)) {
+ return;
+ }
+ const transitionSegment = await this.getSegmentForTransition(entry);
+ const rowToUse = this.getRowToUseFor(entry);
+ const aborted = (await entry.getValue()).aborted;
+ this.drawSegment(transitionSegment.from, transitionSegment.to, rowToUse, aborted);
+ })
+ );
+ this.drawSelectedTransitionEntry();
+ }
+
+ private async getSegmentForTransition(entry: TraceEntry<Transition>): Promise<TimeRange> {
+ const transition = await entry.getValue();
+
+ let createTime: Timestamp;
+ let finishTime: Timestamp;
+ if (entry.getTimestamp().getType() === TimestampType.REAL) {
+ createTime = new RealTimestamp(BigInt(transition.createTime.unixNanos.toString()));
+ finishTime = new RealTimestamp(BigInt(transition.finishTime.unixNanos.toString()));
+ } else if (entry.getTimestamp().getType() === TimestampType.ELAPSED) {
+ createTime = new ElapsedTimestamp(BigInt(transition.createTime.elapsedNanos.toString()));
+ finishTime = new ElapsedTimestamp(BigInt(transition.finishTime.elapsedNanos.toString()));
+ } else {
+ throw new Error('Unspported timestamp type');
+ }
+
+ return {from: createTime, to: finishTime};
+ }
+
+ private drawSegment(start: Timestamp, end: Timestamp, rowToUse: number, aborted: boolean) {
+ const rect = this.getSegmentRect(start, end, rowToUse);
+ const alpha = aborted ? 0.25 : 1.0;
+ this.canvasDrawer.drawRect(rect, this.color, alpha);
+ }
+
+ private async drawSelectedTransitionEntry() {
+ if (this.selectedEntry === undefined || this.shouldNotRenderEntry(this.selectedEntry)) {
+ return;
+ }
+
+ const transitionSegment = await this.getSegmentForTransition(this.selectedEntry);
+
+ const transition = await this.selectedEntry.getValue();
+ const rowIndex = this.getRowToUseFor(this.selectedEntry);
+ const rect = this.getSegmentRect(transitionSegment.from, transitionSegment.to, rowIndex);
+ const alpha = transition.aborted ? 0.25 : 1.0;
+ this.canvasDrawer.drawRect(rect, this.color, alpha);
+ this.canvasDrawer.drawRectBorder(rect);
+ }
+
+ private shouldNotRenderEntry(entry: TraceEntry<Transition>): boolean {
+ return this.shouldNotRenderEntries.includes(entry.getIndex());
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
new file mode 100644
index 0000000..a5bbd3e
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {RealTimestamp} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {waitToBeCalled} from 'test/utils';
+import {TraceType} from 'trace/trace_type';
+import {TransitionTimelineComponent} from './transition_timeline_component';
+
+describe('TransitionTimelineComponent', () => {
+ let fixture: ComponentFixture<TransitionTimelineComponent>;
+ let component: TransitionTimelineComponent;
+ let htmlElement: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ DragDropModule,
+ ],
+ declarations: [TransitionTimelineComponent],
+ })
+ .overrideComponent(TransitionTimelineComponent, {
+ set: {changeDetection: ChangeDetectionStrategy.Default},
+ })
+ .compileComponents();
+ fixture = TestBed.createComponent(TransitionTimelineComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('can draw non-overlapping transitions', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 10n},
+ finishTime: {unixNanos: 30n},
+ } as Transition,
+ {
+ createTime: {unixNanos: 60n},
+ finishTime: {unixNanos: 110n},
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ await waitToBeCalled(drawRectSpy, 2);
+
+ const padding = 5;
+ const oneRowTotalHeight = 30;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(2);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: 0,
+ y: padding,
+ w: Math.floor(width / 5),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor(width / 2),
+ y: padding,
+ w: Math.floor(width / 2),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ });
+
+ it('can draw transitions zoomed in', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 0n},
+ finishTime: {unixNanos: 20n},
+ } as Transition,
+ {
+ createTime: {unixNanos: 60n},
+ finishTime: {unixNanos: 160n},
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ await waitToBeCalled(drawRectSpy, 2);
+
+ const padding = 5;
+ const oneRowTotalHeight = 30;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(2);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: -Math.floor(width / 10),
+ y: padding,
+ w: Math.floor(width / 5),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor(width / 2),
+ y: padding,
+ w: Math.floor(width),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ });
+
+ it('can draw selected entry', async () => {
+ const transition = {
+ createTime: {unixNanos: 35n},
+ finishTime: {unixNanos: 85n},
+ aborted: true,
+ } as Transition;
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([transition])
+ .setTimestamps([new RealTimestamp(35n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+ component.selectedEntry = component.trace.getEntry(0);
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+ const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder');
+
+ const waitPromises = [waitToBeCalled(drawRectSpy, 1), waitToBeCalled(drawRectBorderSpy, 1)];
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ await Promise.all(waitPromises);
+
+ const padding = 5;
+ const oneRowTotalHeight = 30;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ const expectedRect = {
+ x: Math.floor((width * 1) / 4),
+ y: padding,
+ w: Math.floor(width / 2),
+ h: oneRowHeight,
+ };
+ expect(drawRectSpy).toHaveBeenCalledTimes(2); // once drawn as a normal entry another time with rect border
+ expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 0.25);
+
+ expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectBorderSpy).toHaveBeenCalledWith(expectedRect);
+ });
+
+ it('can draw hovering entry', async () => {
+ const transition = {
+ createTime: {unixNanos: 35n},
+ finishTime: {unixNanos: 85n},
+ aborted: true,
+ } as Transition;
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([transition])
+ .setTimestamps([new RealTimestamp(35n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+ const drawRectBorderSpy = spyOn(component.canvasDrawer, 'drawRectBorder');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ const padding = 5;
+ const oneRowTotalHeight = 30;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ component.handleMouseMove({
+ offsetX: Math.floor(width / 2),
+ offsetY: oneRowTotalHeight / 2,
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ } as MouseEvent);
+
+ await waitToBeCalled(drawRectSpy, 1);
+ await waitToBeCalled(drawRectBorderSpy, 1);
+
+ const expectedRect = {
+ x: Math.floor((width * 1) / 4),
+ y: padding,
+ w: Math.floor(width / 2),
+ h: oneRowHeight,
+ };
+ expect(drawRectSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectSpy).toHaveBeenCalledWith(expectedRect, component.color, 0.25);
+
+ expect(drawRectBorderSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectBorderSpy).toHaveBeenCalledWith(expectedRect);
+ });
+
+ it('can draw overlapping transitions (default)', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 10n},
+ finishTime: {unixNanos: 85n},
+ } as Transition,
+ {
+ createTime: {unixNanos: 60n},
+ finishTime: {unixNanos: 110n},
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(10n), new RealTimestamp(60n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ await waitToBeCalled(drawRectSpy, 2);
+
+ const padding = 5;
+ const rows = 2;
+ const oneRowTotalHeight = (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(2);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: 0,
+ y: padding,
+ w: Math.floor((width * 3) / 4),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor(width / 2),
+ y: padding + oneRowTotalHeight,
+ w: Math.floor(width / 2),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ });
+
+ it('can draw overlapping transitions (contained)', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 10n},
+ finishTime: {unixNanos: 85n},
+ } as Transition,
+ {
+ createTime: {unixNanos: 35n},
+ finishTime: {unixNanos: 60n},
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(10n), new RealTimestamp(35n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ await waitToBeCalled(drawRectSpy, 2);
+
+ const padding = 5;
+ const rows = 2;
+ const oneRowTotalHeight = (component.canvasDrawer.getScaledCanvasHeight() - 2 * padding) / rows;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(2);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: 0,
+ y: padding,
+ w: Math.floor((width * 3) / 4),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor(width / 4),
+ y: padding + oneRowTotalHeight,
+ w: Math.floor(width / 4),
+ h: oneRowHeight,
+ },
+ component.color,
+ 1
+ );
+ });
+
+ it('can draw aborted transitions', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 35n},
+ finishTime: {unixNanos: 85n},
+ aborted: true,
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(35n)])
+ .build();
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+ await waitToBeCalled(drawRectSpy, 1);
+
+ const padding = 5;
+ const oneRowTotalHeight = 30;
+ const oneRowHeight = oneRowTotalHeight - padding;
+ const width = component.canvasDrawer.getScaledCanvasWidth();
+
+ expect(drawRectSpy).toHaveBeenCalledTimes(1);
+ expect(drawRectSpy).toHaveBeenCalledWith(
+ {
+ x: Math.floor((width * 1) / 4),
+ y: padding,
+ w: Math.floor(width / 2),
+ h: oneRowHeight,
+ },
+ component.color,
+ 0.25
+ );
+ });
+
+ it('does not render transition with min creation time', async () => {
+ component.trace = new TraceBuilder()
+ .setType(TraceType.TRANSITION)
+ .setEntries([
+ {
+ createTime: {unixNanos: 10n, isMin: true},
+ finishTime: {unixNanos: 30n},
+ } as Transition,
+ ])
+ .setTimestamps([new RealTimestamp(10n)])
+ .build();
+ component.shouldNotRenderEntries.push(0);
+ component.selectionRange = {from: new RealTimestamp(10n), to: new RealTimestamp(110n)};
+
+ const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
+
+ fixture.detectChanges();
+ await fixture.whenRenderingDone();
+
+ expect(drawRectSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts
similarity index 60%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts
index dd03545..d66484a 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler.ts
@@ -14,10 +14,17 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+import {DraggableCanvasObject} from './draggable_canvas_object';
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+export type DragListener = (x: number, y: number) => void;
+export type DropListener = DragListener;
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
+export interface CanvasMouseHandler {
+ registerDraggableObject(
+ draggableObject: DraggableCanvasObject,
+ onDrag: DragListener,
+ onDrop: DropListener
+ ): void;
+
+ notifyDrawnOnTop(draggableObject: DraggableCanvasObject): void;
}
diff --git a/tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
similarity index 72%
rename from tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
index 4eb3bea..dccdd8d 100644
--- a/tools/winscope/src/app/components/canvas/canvas_mouse_handler.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/canvas_mouse_handler_impl.ts
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-import {CanvasDrawer} from './canvas_drawer';
+import {assertDefined} from 'common/assert_utils';
+import {Point} from 'common/geometry_utils';
+import {CanvasMouseHandler, DragListener, DropListener} from './canvas_mouse_handler';
import {DraggableCanvasObject} from './draggable_canvas_object';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
-export type DragListener = (x: number, y: number) => void;
-export type DropListener = DragListener;
-
-export class CanvasMouseHandler {
+export class CanvasMouseHandlerImpl implements CanvasMouseHandler {
// Ordered top most element to bottom most
private draggableObjects: DraggableCanvasObject[] = [];
private draggingObject: DraggableCanvasObject | undefined = undefined;
@@ -29,9 +29,9 @@
private onDrop = new Map<DraggableCanvasObject, DropListener>();
constructor(
- private drawer: CanvasDrawer,
+ private drawer: MiniTimelineDrawer,
private defaultCursor: string = 'auto',
- private onUnhandledMouseDown: (x: number, y: number) => void = (x, y) => {}
+ private onUnhandledMouseDown: (point: Point) => void = (point) => {}
) {
this.drawer.canvas.addEventListener('mousemove', (event) => {
this.handleMouseMove(event);
@@ -67,49 +67,49 @@
private handleMouseDown(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
- const {mouseX, mouseY} = this.getPos(e);
+ const mousePoint = this.getPos(e);
- const clickedObject = this.objectAt(mouseX, mouseY);
+ const clickedObject = this.objectAt(mousePoint);
if (clickedObject !== undefined) {
this.draggingObject = clickedObject;
} else {
- this.onUnhandledMouseDown(mouseX, mouseY);
+ this.onUnhandledMouseDown(mousePoint);
}
- this.updateCursor(mouseX, mouseY);
+ this.updateCursor(mousePoint);
}
private handleMouseMove(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
- const {mouseX, mouseY} = this.getPos(e);
+ const mousePoint = this.getPos(e);
if (this.draggingObject !== undefined) {
const onDragCallback = this.onDrag.get(this.draggingObject);
if (onDragCallback !== undefined) {
- onDragCallback(mouseX, mouseY);
+ onDragCallback(mousePoint.x, mousePoint.y);
}
}
- this.updateCursor(mouseX, mouseY);
+ this.updateCursor(mousePoint);
}
private handleMouseUp(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
- const {mouseX, mouseY} = this.getPos(e);
+ const mousePoint = this.getPos(e);
if (this.draggingObject !== undefined) {
const onDropCallback = this.onDrop.get(this.draggingObject);
if (onDropCallback !== undefined) {
- onDropCallback(mouseX, mouseY);
+ onDropCallback(mousePoint.x, mousePoint.y);
}
}
this.draggingObject = undefined;
- this.updateCursor(mouseX, mouseY);
+ this.updateCursor(mousePoint);
}
- private getPos(e: MouseEvent) {
+ private getPos(e: MouseEvent): Point {
let mouseX = e.offsetX;
const mouseY = e.offsetY;
@@ -121,11 +121,11 @@
mouseX = this.drawer.getWidth() - this.drawer.padding.right;
}
- return {mouseX, mouseY};
+ return {x: mouseX, y: mouseY};
}
- private updateCursor(mouseX: number, mouseY: number) {
- const hoverObject = this.objectAt(mouseX, mouseY);
+ private updateCursor(mousePoint: Point) {
+ const hoverObject = this.objectAt(mousePoint);
if (hoverObject !== undefined) {
if (hoverObject === this.draggingObject) {
this.drawer.canvas.style.cursor = 'grabbing';
@@ -137,15 +137,11 @@
}
}
- private objectAt(mouseX: number, mouseY: number): DraggableCanvasObject | undefined {
+ private objectAt(mousePoint: Point): DraggableCanvasObject | undefined {
for (const object of this.draggableObjects) {
- object.definePath(this.drawer.ctx);
- if (
- this.drawer.ctx.isPointInPath(
- mouseX * this.drawer.getXScale(),
- mouseY * this.drawer.getYScale()
- )
- ) {
+ const ctx = assertDefined(this.drawer.canvas.getContext('2d'));
+ object.definePath(ctx);
+ if (ctx.isPointInPath(mousePoint.x, mousePoint.y)) {
return object;
}
}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts
similarity index 80%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts
index d2b1744..7125ea3 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
-
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export interface DraggableCanvasObject {
+ draw(ctx: CanvasRenderingContext2D): void;
+ definePath(ctx: CanvasRenderingContext2D): void;
}
diff --git a/tools/winscope/src/app/components/canvas/draggable_canvas_object.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
similarity index 88%
rename from tools/winscope/src/app/components/canvas/draggable_canvas_object.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
index 65b1032..38acbe2 100644
--- a/tools/winscope/src/app/components/canvas/draggable_canvas_object.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/draggable_canvas_object_impl.ts
@@ -15,19 +15,20 @@
*/
import {MathUtils} from 'three/src/Three';
-import {Segment} from '../timeline/utils';
-import {CanvasDrawer} from './canvas_drawer';
+import {Segment} from '../../utils';
+import {DraggableCanvasObject} from './draggable_canvas_object';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
export interface DrawConfig {
fillStyle: string;
fill: boolean;
}
-export class DraggableCanvasObject {
+export class DraggableCanvasObjectImpl implements DraggableCanvasObject {
private draggingPosition: number | undefined;
constructor(
- private drawer: CanvasDrawer,
+ private drawer: MiniTimelineDrawer,
private positionGetter: () => number,
private definePathFunc: (ctx: CanvasRenderingContext2D, position: number) => void,
private drawConfig: DrawConfig,
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts
new file mode 100644
index 0000000..10d66b1
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_canvas_drawer_data.ts
@@ -0,0 +1,39 @@
+import {TraceType} from 'trace/trace_type';
+import {Segment} from '../../utils';
+import {Transformer} from '../transformer';
+import {MiniTimelineDrawerOutput} from './mini_timeline_drawer_output';
+
+export type TimelineEntries = Map<
+ TraceType,
+ {
+ points: number[];
+ segments: Segment[];
+ activePoint: number | undefined;
+ activeSegment: Segment | undefined;
+ }
+>;
+
+export class MiniCanvasDrawerData {
+ constructor(
+ public selectedPosition: number,
+ public selection: Segment,
+ private timelineEntriesGetter: () => Promise<TimelineEntries>,
+ public transformer: Transformer
+ ) {}
+
+ private entries: TimelineEntries | undefined = undefined;
+
+ async getTimelineEntries(): Promise<TimelineEntries> {
+ if (this.entries === undefined) {
+ this.entries = await this.timelineEntriesGetter();
+ }
+ return this.entries;
+ }
+
+ toOutput(): MiniTimelineDrawerOutput {
+ return new MiniTimelineDrawerOutput(this.transformer.untransform(this.selectedPosition), {
+ from: this.transformer.untransform(this.selection.from),
+ to: this.transformer.untransform(this.selection.to),
+ });
+ }
+}
diff --git a/tools/winscope/src/app/components/canvas/canvas_drawer.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
similarity index 74%
rename from tools/winscope/src/app/components/canvas/canvas_drawer.ts
rename to tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
index 03e8e83..0670aa4 100644
--- a/tools/winscope/src/app/components/canvas/canvas_drawer.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,23 +14,16 @@
* limitations under the License.
*/
+import {Padding} from 'common/padding';
import {CanvasMouseHandler} from './canvas_mouse_handler';
-export interface Padding {
- left: number;
- top: number;
- right: number;
- bottom: number;
-}
-
-export interface CanvasDrawer {
- draw(): void;
- handler: CanvasMouseHandler;
- canvas: HTMLCanvasElement;
- ctx: CanvasRenderingContext2D;
- padding: Padding;
+export interface MiniTimelineDrawer {
+ draw(): Promise<void>;
getXScale(): number;
getYScale(): number;
getWidth(): number;
- getHeight(): number;
+ canvas: HTMLCanvasElement;
+ handler: CanvasMouseHandler;
+ padding: Padding;
+ usableRange: {from: number; to: number};
}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
new file mode 100644
index 0000000..e739128
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_impl.ts
@@ -0,0 +1,236 @@
+/*
+ * 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.
+ */
+
+import {Color} from 'app/colors';
+import {TRACE_INFO} from 'app/trace_info';
+import {Point} from 'common/geometry_utils';
+import {Padding} from 'common/padding';
+import {Timestamp} from 'common/time';
+import {CanvasMouseHandler} from './canvas_mouse_handler';
+import {CanvasMouseHandlerImpl} from './canvas_mouse_handler_impl';
+import {DraggableCanvasObject} from './draggable_canvas_object';
+import {DraggableCanvasObjectImpl} from './draggable_canvas_object_impl';
+import {MiniCanvasDrawerData, TimelineEntries} from './mini_canvas_drawer_data';
+import {MiniTimelineDrawer} from './mini_timeline_drawer';
+import {MiniTimelineDrawerInput} from './mini_timeline_drawer_input';
+
+export class MiniTimelineDrawerImpl implements MiniTimelineDrawer {
+ ctx: CanvasRenderingContext2D;
+ handler: CanvasMouseHandler;
+
+ private activePointer: DraggableCanvasObject;
+
+ private get pointerWidth() {
+ return this.getHeight() / 6;
+ }
+
+ getXScale() {
+ return this.ctx.getTransform().m11;
+ }
+
+ getYScale() {
+ return this.ctx.getTransform().m22;
+ }
+
+ getWidth() {
+ return this.canvas.width / this.getXScale();
+ }
+
+ getHeight() {
+ return this.canvas.height / this.getYScale();
+ }
+
+ get usableRange() {
+ return {
+ from: this.padding.left,
+ to: this.getWidth() - this.padding.left - this.padding.right,
+ };
+ }
+
+ get input(): MiniCanvasDrawerData {
+ return this.inputGetter().transform(this.usableRange);
+ }
+
+ constructor(
+ public canvas: HTMLCanvasElement,
+ private inputGetter: () => MiniTimelineDrawerInput,
+ private onPointerPositionDragging: (pos: Timestamp) => void,
+ private onPointerPositionChanged: (pos: Timestamp) => void,
+ private onUnhandledClick: (pos: Timestamp) => void
+ ) {
+ const ctx = canvas.getContext('2d');
+
+ if (ctx === null) {
+ throw Error('MiniTimeline canvas context was null!');
+ }
+
+ this.ctx = ctx;
+
+ const onUnhandledClickInternal = async (mousePoint: Point) => {
+ this.onUnhandledClick(this.input.transformer.untransform(mousePoint.x));
+ };
+ this.handler = new CanvasMouseHandlerImpl(this, 'pointer', onUnhandledClickInternal);
+
+ this.activePointer = new DraggableCanvasObjectImpl(
+ this,
+ () => this.selectedPosition,
+ (ctx: CanvasRenderingContext2D, position: number) => {
+ const barWidth = 3;
+ const triangleHeight = this.pointerWidth / 2;
+
+ ctx.beginPath();
+ ctx.moveTo(position - triangleHeight, 0);
+ ctx.lineTo(position + triangleHeight, 0);
+ ctx.lineTo(position + barWidth / 2, triangleHeight);
+ ctx.lineTo(position + barWidth / 2, this.getHeight());
+ ctx.lineTo(position - barWidth / 2, this.getHeight());
+ ctx.lineTo(position - barWidth / 2, triangleHeight);
+ ctx.closePath();
+ },
+ {
+ fillStyle: Color.ACTIVE_POINTER,
+ fill: true,
+ },
+ (x) => {
+ this.input.selectedPosition = x;
+ this.onPointerPositionDragging(this.input.transformer.untransform(x));
+ },
+ (x) => {
+ this.input.selectedPosition = x;
+ this.onPointerPositionChanged(this.input.transformer.untransform(x));
+ },
+ () => this.usableRange
+ );
+ }
+
+ get selectedPosition() {
+ return this.input.selectedPosition;
+ }
+
+ get selection() {
+ return this.input.selection;
+ }
+
+ async getTimelineEntries(): Promise<TimelineEntries> {
+ return await this.input.getTimelineEntries();
+ }
+
+ get padding(): Padding {
+ return {
+ top: Math.ceil(this.getHeight() / 5),
+ bottom: Math.ceil(this.getHeight() / 5),
+ left: Math.ceil(this.pointerWidth / 2),
+ right: Math.ceil(this.pointerWidth / 2),
+ };
+ }
+
+ get innerHeight() {
+ return this.getHeight() - this.padding.top - this.padding.bottom;
+ }
+
+ async draw() {
+ this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
+
+ await this.drawTraceLines();
+
+ this.drawTimelineGuides();
+
+ this.activePointer.draw(this.ctx);
+ }
+
+ private async drawTraceLines() {
+ const lineHeight = this.innerHeight / 8;
+
+ let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
+
+ (await this.getTimelineEntries()).forEach((entries, traceType) => {
+ this.ctx.globalAlpha = 0.7;
+ this.ctx.fillStyle = TRACE_INFO[traceType].color;
+ this.ctx.strokeStyle = 'blue';
+
+ for (const entry of entries.points) {
+ const width = 5;
+ this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
+ }
+
+ for (const entry of entries.segments) {
+ const width = Math.max(entry.to - entry.from, 3);
+ this.ctx.fillRect(entry.from, fromTop, width, lineHeight);
+ }
+
+ this.ctx.fillStyle = Color.ACTIVE_POINTER;
+ if (entries.activePoint) {
+ const entry = entries.activePoint;
+ const width = 5;
+ this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
+ }
+
+ if (entries.activeSegment) {
+ const entry = entries.activeSegment;
+ const width = Math.max(entry.to - entry.from, 3);
+ this.ctx.fillRect(entry.from, fromTop, width, lineHeight);
+ }
+
+ this.ctx.globalAlpha = 1.0;
+
+ fromTop -= (lineHeight * 4) / 3;
+ });
+ }
+
+ private drawTimelineGuides() {
+ const edgeBarHeight = (this.innerHeight * 1) / 2;
+ const edgeBarWidth = 4;
+
+ const boldBarHeight = (this.innerHeight * 1) / 5;
+ const boldBarWidth = edgeBarWidth;
+
+ const lightBarHeight = (this.innerHeight * 1) / 6;
+ const lightBarWidth = 2;
+
+ const minSpacing = lightBarWidth * 7;
+ const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
+ const barSets = Math.floor(
+ (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
+ );
+ const bars = barSets * 10;
+ const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
+ let start = edgeBarWidth + spacing;
+ for (let i = 1; i < bars; i++) {
+ if (i % 10 === 0) {
+ // Draw boldbar
+ this.ctx.fillStyle = Color.GUIDE_BAR;
+ this.ctx.fillRect(
+ start,
+ this.padding.top + this.innerHeight - boldBarHeight,
+ boldBarWidth,
+ boldBarHeight
+ );
+ start += boldBarWidth; // TODO: Shift a bit
+ } else {
+ // Draw lightbar
+ this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
+ this.ctx.fillRect(
+ start,
+ this.padding.top + this.innerHeight - lightBarHeight,
+ lightBarWidth,
+ lightBarHeight
+ );
+ start += lightBarWidth;
+ }
+ start += spacing;
+ }
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
new file mode 100644
index 0000000..9201bc4
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
@@ -0,0 +1,125 @@
+import {TimelineData} from 'app/timeline_data';
+import {ElapsedTimestamp, RealTimestamp, TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
+import {Trace, TraceEntry} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceType} from 'trace/trace_type';
+import {Segment} from '../../utils';
+import {Transformer} from '../transformer';
+import {MiniCanvasDrawerData, TimelineEntries} from './mini_canvas_drawer_data';
+
+export class MiniTimelineDrawerInput {
+ constructor(
+ public fullRange: TimeRange,
+ public selectedPosition: Timestamp,
+ public selection: TimeRange,
+ public zoomRange: TimeRange,
+ public traces: Traces,
+ public timelineData: TimelineData
+ ) {}
+
+ transform(mapToRange: Segment): MiniCanvasDrawerData {
+ const transformer = new Transformer(this.zoomRange, mapToRange);
+
+ return new MiniCanvasDrawerData(
+ transformer.transform(this.selectedPosition),
+ {
+ from: transformer.transform(this.selection.from),
+ to: transformer.transform(this.selection.to),
+ },
+ () => {
+ return this.transformTracesTimestamps(transformer);
+ },
+ transformer
+ );
+ }
+
+ private async transformTracesTimestamps(transformer: Transformer): Promise<TimelineEntries> {
+ const transformedTraceSegments = new Map<
+ TraceType,
+ {
+ points: number[];
+ segments: Segment[];
+ activePoint: number | undefined;
+ activeSegment: Segment | undefined;
+ }
+ >();
+
+ await Promise.all(
+ this.traces.mapTrace(async (trace, type) => {
+ const activeEntry = this.timelineData.findCurrentEntryFor(trace.type);
+
+ if (type === TraceType.TRANSITION) {
+ // Transition trace is a special case, with entries with time ranges
+ const transitionTrace = this.traces.getTrace(type)!;
+ transformedTraceSegments.set(trace.type, {
+ points: [],
+ activePoint: undefined,
+ segments: await this.transformTransitionTraceTimestamps(transformer, transitionTrace),
+ activeSegment: activeEntry
+ ? await this.transformTransitionEntry(transformer, activeEntry)
+ : undefined,
+ });
+ } else {
+ transformedTraceSegments.set(trace.type, {
+ points: this.transformTraceTimestamps(transformer, trace),
+ activePoint: activeEntry
+ ? transformer.transform(activeEntry.getTimestamp())
+ : undefined,
+ segments: [],
+ activeSegment: undefined,
+ });
+ }
+ })
+ );
+
+ return transformedTraceSegments;
+ }
+
+ private async transformTransitionTraceTimestamps(
+ transformer: Transformer,
+ trace: Trace<Transition>
+ ): Promise<Segment[]> {
+ const promises: Array<Promise<Segment | undefined>> = [];
+ trace.forEachEntry((entry) => {
+ promises.push(this.transformTransitionEntry(transformer, entry));
+ });
+
+ return (await Promise.all(promises)).filter((it) => it !== undefined) as Segment[];
+ }
+
+ private async transformTransitionEntry(
+ transformer: Transformer,
+ entry: TraceEntry<Transition>
+ ): Promise<Segment | undefined> {
+ const transition = await entry.getValue();
+ let createTime: Timestamp;
+ let finishTime: Timestamp;
+
+ if (transition.createTime.isMin || transition.finishTime.isMax) {
+ return undefined;
+ }
+
+ if (entry.getTimestamp().getType() === TimestampType.REAL) {
+ createTime = new RealTimestamp(BigInt(transition.createTime.unixNanos.toString()));
+ finishTime = new RealTimestamp(BigInt(transition.finishTime.unixNanos.toString()));
+ } else if (entry.getTimestamp().getType() === TimestampType.ELAPSED) {
+ createTime = new ElapsedTimestamp(BigInt(transition.createTime.elapsedNanos.toString()));
+ finishTime = new ElapsedTimestamp(BigInt(transition.finishTime.elapsedNanos.toString()));
+ } else {
+ throw new Error('Unspported timestamp type');
+ }
+
+ return {from: transformer.transform(createTime), to: transformer.transform(finishTime)};
+ }
+
+ private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
+ const result: number[] = [];
+
+ trace.forEachTimestamp((timestamp) => {
+ result.push(transformer.transform(timestamp));
+ });
+
+ return result;
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts
new file mode 100644
index 0000000..7a496dc
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_output.ts
@@ -0,0 +1,5 @@
+import {TimeRange, Timestamp} from 'common/time';
+
+export class MiniTimelineDrawerOutput {
+ constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
new file mode 100644
index 0000000..3804747
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
@@ -0,0 +1,345 @@
+/*
+ * 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.
+ */
+
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ Output,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core';
+import {TimelineData} from 'app/timeline_data';
+import {assertDefined} from 'common/assert_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {Traces} from 'trace/traces';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType, TraceTypeUtils} from 'trace/trace_type';
+import {MiniTimelineDrawer} from './drawer/mini_timeline_drawer';
+import {MiniTimelineDrawerImpl} from './drawer/mini_timeline_drawer_impl';
+import {MiniTimelineDrawerInput} from './drawer/mini_timeline_drawer_input';
+import {Transformer} from './transformer';
+
+@Component({
+ selector: 'mini-timeline',
+ template: `
+ <div id="mini-timeline-wrapper" #miniTimelineWrapper>
+ <canvas #canvas id="mini-timeline-canvas"></canvas>
+ <div class="zoom-control-wrapper">
+ <div class="zoom-control">
+ <div class="zoom-buttons">
+ <button mat-icon-button id="reset-zoom-btn" (click)="resetZoom()">
+ <mat-icon>refresh</mat-icon>
+ </button>
+ <button mat-icon-button id="zoom-in-btn" (click)="zoomIn()">
+ <mat-icon>zoom_in</mat-icon>
+ </button>
+ <button mat-icon-button id="zoom-out-btn" (click)="zoomOut()">
+ <mat-icon>zoom_out</mat-icon>
+ </button>
+ </div>
+ <slider
+ [fullRange]="timelineData.getFullTimeRange()"
+ [zoomRange]="timelineData.getZoomRange()"
+ [currentPosition]="timelineData.getCurrentPosition()"
+ (onZoomChanged)="onZoomChanged($event)"></slider>
+ </div>
+ </div>
+ </div>
+ `,
+ styles: [
+ `
+ #mini-timeline-wrapper {
+ width: 100%;
+ min-height: 5em;
+ height: 100%;
+ }
+ .zoom-control-wrapper {
+ margin-top: -25px;
+ margin-left: -60px;
+ padding-right: 30px;
+ }
+ .zoom-control {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ justify-items: center;
+ gap: 23px;
+ }
+ .zoom-control slider {
+ flex-grow: 1;
+ }
+ `,
+ ],
+})
+export class MiniTimelineComponent {
+ @Input() timelineData!: TimelineData;
+ @Input() currentTracePosition!: TracePosition;
+ @Input() selectedTraces!: TraceType[];
+
+ @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
+ @Output() onSeekTimestampUpdate = new EventEmitter<Timestamp | undefined>();
+
+ @ViewChild('miniTimelineWrapper', {static: false}) miniTimelineWrapper!: ElementRef;
+ @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
+ get canvas(): HTMLCanvasElement {
+ return this.canvasRef.nativeElement;
+ }
+
+ drawer: MiniTimelineDrawer | undefined = undefined;
+
+ ngAfterViewInit(): void {
+ this.makeHiPPICanvas();
+
+ const updateTimestampCallback = (timestamp: Timestamp) => {
+ this.onSeekTimestampUpdate.emit(undefined);
+ this.onTracePositionUpdate.emit(this.timelineData.makePositionFromActiveTrace(timestamp));
+ };
+
+ this.drawer = new MiniTimelineDrawerImpl(
+ this.canvas,
+ () => this.getMiniCanvasDrawerInput(),
+ (position) => this.onSeekTimestampUpdate.emit(position),
+ updateTimestampCallback,
+ updateTimestampCallback
+ );
+ this.drawer.draw();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (this.drawer !== undefined) {
+ this.drawer.draw();
+ }
+ }
+
+ isZoomed(): boolean {
+ const fullRange = this.timelineData.getFullTimeRange();
+ const zoomRange = this.timelineData.getZoomRange();
+ return fullRange.from !== zoomRange.from || fullRange.to !== zoomRange.to;
+ }
+
+ private getMiniCanvasDrawerInput() {
+ return new MiniTimelineDrawerInput(
+ this.timelineData.getFullTimeRange(),
+ this.currentTracePosition.timestamp,
+ this.timelineData.getSelectionTimeRange(),
+ this.timelineData.getZoomRange(),
+ this.getTracesToShow(),
+ this.timelineData
+ );
+ }
+
+ getTracesToShow(): Traces {
+ const traces = new Traces();
+ this.selectedTraces
+ .filter((type) => this.timelineData.getTraces().getTrace(type) !== undefined)
+ .sort((a, b) => TraceTypeUtils.compareByDisplayOrder(b, a)) // reversed to ensure display is ordered top to bottom
+ .forEach((type) => {
+ traces.setTrace(type, assertDefined(this.timelineData.getTraces().getTrace(type)));
+ });
+ return traces;
+ }
+
+ private makeHiPPICanvas() {
+ // Reset any size before computing new size to avoid it interfering with size computations
+ this.canvas.width = 0;
+ this.canvas.height = 0;
+ this.canvas.style.width = 'auto';
+ this.canvas.style.height = 'auto';
+
+ const width = this.miniTimelineWrapper.nativeElement.offsetWidth;
+ const height = this.miniTimelineWrapper.nativeElement.offsetHeight;
+
+ const HiPPIwidth = window.devicePixelRatio * width;
+ const HiPPIheight = window.devicePixelRatio * height;
+
+ this.canvas.width = HiPPIwidth;
+ this.canvas.height = HiPPIheight;
+ this.canvas.style.width = width + 'px';
+ this.canvas.style.height = height + 'px';
+
+ // ensure all drawing operations are scaled
+ if (window.devicePixelRatio !== 1) {
+ const context = this.canvas.getContext('2d')!;
+ context.scale(window.devicePixelRatio, window.devicePixelRatio);
+ }
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event: Event) {
+ this.makeHiPPICanvas();
+ this.drawer?.draw();
+ }
+
+ onZoomChanged(zoom: TimeRange) {
+ this.timelineData.setZoom(zoom);
+ this.timelineData.setSelectionTimeRange(zoom);
+ this.drawer?.draw();
+ }
+
+ resetZoom() {
+ this.onZoomChanged(this.timelineData.getFullTimeRange());
+ }
+
+ zoomIn(zoomOn: Timestamp | undefined = undefined) {
+ this.zoom({nominator: 3n, denominator: 4n}, zoomOn);
+ }
+
+ zoomOut(zoomOn: Timestamp | undefined = undefined) {
+ this.zoom({nominator: 5n, denominator: 4n}, zoomOn);
+ }
+
+ zoom(
+ zoomRatio: {nominator: bigint; denominator: bigint},
+ zoomOn: Timestamp | undefined = undefined
+ ) {
+ const fullRange = this.timelineData.getFullTimeRange();
+ const currentZoomRange = this.timelineData.getZoomRange();
+ const currentZoomWidth = currentZoomRange.to.minus(currentZoomRange.from);
+ const zoomToWidth = currentZoomWidth.times(zoomRatio.nominator).div(zoomRatio.denominator);
+
+ const cursorPosition = this.timelineData.getCurrentPosition()?.timestamp;
+ const currentMiddle = currentZoomRange.from.plus(currentZoomRange.to).div(2n);
+
+ let newFrom: Timestamp;
+ let newTo: Timestamp;
+ if (zoomOn === undefined) {
+ let zoomTowards = currentMiddle;
+ if (cursorPosition !== undefined && cursorPosition.in(currentZoomRange)) {
+ zoomTowards = cursorPosition;
+ }
+
+ let leftAdjustment;
+ let rightAdjustment;
+ if (zoomTowards.getValueNs() < currentMiddle.getValueNs()) {
+ leftAdjustment = currentZoomWidth.times(0n);
+ rightAdjustment = currentZoomWidth
+ .times(zoomRatio.denominator - zoomRatio.nominator)
+ .div(zoomRatio.denominator);
+ } else {
+ leftAdjustment = currentZoomWidth
+ .times(zoomRatio.denominator - zoomRatio.nominator)
+ .div(zoomRatio.denominator);
+ rightAdjustment = currentZoomWidth.times(0n);
+ }
+
+ newFrom = currentZoomRange.from.plus(leftAdjustment);
+ newTo = currentZoomRange.to.minus(rightAdjustment);
+ const newMiddle = newFrom.plus(newTo).div(2n);
+
+ if (
+ (zoomTowards.getValueNs() <= currentMiddle.getValueNs() &&
+ newMiddle.getValueNs() < zoomTowards.getValueNs()) ||
+ (zoomTowards.getValueNs() >= currentMiddle.getValueNs() &&
+ newMiddle.getValueNs() > zoomTowards.getValueNs())
+ ) {
+ // Moved past middle, so ensure cursor is in the middle
+ newFrom = zoomTowards.minus(zoomToWidth.div(2n));
+ newTo = zoomTowards.plus(zoomToWidth.div(2n));
+ }
+ } else {
+ newFrom = zoomOn.minus(zoomToWidth.div(2n));
+ newTo = zoomOn.plus(zoomToWidth.div(2n));
+ }
+
+ if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
+ newTo = TimeUtils.min(fullRange.to, newTo.plus(fullRange.from.minus(newFrom)));
+ newFrom = fullRange.from;
+ }
+
+ if (newTo.getValueNs() > fullRange.to.getValueNs()) {
+ newFrom = TimeUtils.max(fullRange.from, newFrom.minus(newTo.minus(fullRange.to)));
+ newTo = fullRange.to;
+ }
+
+ this.onZoomChanged({
+ from: newFrom,
+ to: newTo,
+ });
+ }
+
+ // -1 for x direction, 1 for y direction
+ private lastMoves: WheelEvent[] = [];
+ @HostListener('wheel', ['$event'])
+ onScroll(event: WheelEvent) {
+ this.lastMoves.push(event);
+ setTimeout(() => this.lastMoves.shift(), 1000);
+
+ const xMoveAmount = this.lastMoves.reduce((accumulator, it) => accumulator + it.deltaX, 0);
+ const yMoveAmount = this.lastMoves.reduce((accumulator, it) => accumulator + it.deltaY, 0);
+
+ let moveDirection: 'x' | 'y';
+ if (Math.abs(yMoveAmount) > Math.abs(xMoveAmount)) {
+ moveDirection = 'y';
+ } else {
+ moveDirection = 'x';
+ }
+
+ if (
+ (event.target as any)?.id === 'mini-timeline-canvas' &&
+ event.deltaY !== 0 &&
+ moveDirection === 'y'
+ ) {
+ // Zooming
+ const canvas = event.target as HTMLCanvasElement;
+ const xPosInCanvas = event.x - canvas.offsetLeft;
+ const zoomRange = this.timelineData.getZoomRange();
+
+ const zoomTo = new Transformer(zoomRange, assertDefined(this.drawer).usableRange).untransform(
+ xPosInCanvas
+ );
+
+ if (event.deltaY < 0) {
+ this.zoomIn(zoomTo);
+ } else {
+ this.zoomOut(zoomTo);
+ }
+ }
+
+ if (event.deltaX !== 0 && moveDirection === 'x') {
+ // Horizontal scrolling
+ const scrollAmount = event.deltaX;
+ const fullRange = this.timelineData.getFullTimeRange();
+ const zoomRange = this.timelineData.getZoomRange();
+
+ const usableRange = assertDefined(this.drawer).usableRange;
+ const transformer = new Transformer(zoomRange, usableRange);
+ const shiftAmount = transformer
+ .untransform(usableRange.from + scrollAmount)
+ .minus(zoomRange.from);
+ let newFrom = zoomRange.from.plus(shiftAmount);
+ let newTo = zoomRange.to.plus(shiftAmount);
+
+ if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
+ newTo = newTo.plus(fullRange.from.minus(newFrom));
+ newFrom = fullRange.from;
+ }
+
+ if (newTo.getValueNs() > fullRange.to.getValueNs()) {
+ newFrom = newFrom.minus(newTo.minus(fullRange.to));
+ newTo = fullRange.to;
+ }
+
+ this.onZoomChanged({
+ from: newFrom,
+ to: newTo,
+ });
+ }
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
new file mode 100644
index 0000000..516ee9b
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {TimelineData} from 'app/timeline_data';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
+import {TracesBuilder} from 'test/unit/traces_builder';
+import {dragElement} from 'test/utils';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {MiniTimelineComponent} from './mini_timeline_component';
+import {SliderComponent} from './slider_component';
+
+describe('MiniTimelineComponent', () => {
+ let fixture: ComponentFixture<MiniTimelineComponent>;
+ let component: MiniTimelineComponent;
+ let htmlElement: HTMLElement;
+ let timelineData: TimelineData;
+
+ const timestamp10 = new RealTimestamp(10n);
+ const timestamp20 = new RealTimestamp(20n);
+ const timestamp1000 = new RealTimestamp(1000n);
+
+ const position800 = TracePosition.fromTimestamp(new RealTimestamp(800n));
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ DragDropModule,
+ ],
+ declarations: [MiniTimelineComponent, SliderComponent],
+ })
+ .overrideComponent(MiniTimelineComponent, {
+ set: {changeDetection: ChangeDetectionStrategy.Default},
+ })
+ .compileComponents();
+ fixture = TestBed.createComponent(MiniTimelineComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+
+ timelineData = new TimelineData();
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.TRANSACTIONS, [timestamp10, timestamp20])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp20])
+ .build();
+ timelineData.initialize(traces, undefined);
+ component.timelineData = timelineData;
+ expect(timelineData.getCurrentPosition()).toBeTruthy();
+ component.currentTracePosition = timelineData.getCurrentPosition()!;
+ component.selectedTraces = [TraceType.SURFACE_FLINGER];
+
+ fixture.detectChanges();
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('redraws on resize', () => {
+ const spy = spyOn(assertDefined(component.drawer), 'draw');
+ expect(spy).not.toHaveBeenCalled();
+
+ component.onResize({} as Event);
+
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('resets zoom on reset zoom button click', () => {
+ const expectedZoomRange = {
+ from: new RealTimestamp(15n),
+ to: new RealTimestamp(16n),
+ };
+ timelineData.setZoom(expectedZoomRange);
+
+ let zoomRange = timelineData.getZoomRange();
+ let fullRange = timelineData.getFullTimeRange();
+ expect(zoomRange).toBe(expectedZoomRange);
+ expect(fullRange.from).toBe(timestamp10);
+ expect(fullRange.to).toBe(timestamp20);
+
+ fixture.detectChanges();
+
+ const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+ assertDefined(zoomButton).click();
+
+ zoomRange = timelineData.getZoomRange();
+ fullRange = timelineData.getFullTimeRange();
+ expect(zoomRange).toBe(fullRange);
+ });
+
+ it('show zoom controls when zoomed out', () => {
+ const zoomControlDiv = htmlElement.querySelector('.zoom-control');
+ expect(zoomControlDiv).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(zoomControlDiv)).visibility).toBe('visible');
+
+ const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(zoomButton)).visibility).toBe('visible');
+ });
+
+ it('shows zoom controls when zoomed in', () => {
+ const zoom = {
+ from: new RealTimestamp(15n),
+ to: new RealTimestamp(16n),
+ };
+ timelineData.setZoom(zoom);
+
+ fixture.detectChanges();
+
+ const zoomControlDiv = htmlElement.querySelector('.zoom-control');
+ expect(zoomControlDiv).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(zoomControlDiv)).visibility).toBe('visible');
+
+ const zoomButton = htmlElement.querySelector('button#reset-zoom-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(zoomButton)).visibility).toBe('visible');
+ });
+
+ it('loads zoomed out', () => {
+ expect(component.isZoomed()).toBeFalse();
+ });
+
+ it('updates timelinedata on zoom changed', () => {
+ const zoom = {
+ from: new RealTimestamp(15n),
+ to: new RealTimestamp(16n),
+ };
+ component.onZoomChanged(zoom);
+ expect(timelineData.getZoomRange()).toBe(zoom);
+ });
+
+ it('creates an appropriately sized canvas', () => {
+ expect(component.canvas.width).toBeGreaterThan(100);
+ expect(component.canvas.height).toBeGreaterThan(10);
+ });
+
+ it('getTracesToShow returns traces targeted by selectedTraces', () => {
+ const traces = component.getTracesToShow();
+ const types: TraceType[] = [];
+ traces.forEachTrace((trace) => {
+ types.push(trace.type);
+ });
+ expect(types).toHaveSize(component.selectedTraces.length);
+ for (const type of component.selectedTraces) {
+ expect(types).toContain(type);
+ }
+ });
+
+ it('getTracesToShow adds traces in correct order', () => {
+ component.selectedTraces = [
+ TraceType.WINDOW_MANAGER,
+ TraceType.SURFACE_FLINGER,
+ TraceType.TRANSACTIONS,
+ ];
+ const traces = component.getTracesToShow().mapTrace((trace, type) => trace.type);
+ expect(traces).toEqual([
+ TraceType.TRANSACTIONS,
+ TraceType.WINDOW_MANAGER,
+ TraceType.SURFACE_FLINGER,
+ ]);
+ });
+
+ it('moving slider around updates zoom', fakeAsync(async () => {
+ const initialZoom = {
+ from: new RealTimestamp(15n),
+ to: new RealTimestamp(16n),
+ };
+ component.onZoomChanged(initialZoom);
+
+ fixture.detectChanges();
+
+ const slider = htmlElement.querySelector('.slider .handle');
+ expect(slider).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(slider), 100, 8);
+
+ const finalZoom = timelineData.getZoomRange();
+ expect(finalZoom).not.toBe(initialZoom);
+ }));
+
+ it('zoom button zooms onto cursor', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ let initialZoom = {
+ from: timestamp10,
+ to: timestamp1000,
+ };
+ component.onZoomChanged(initialZoom);
+
+ component.timelineData.setPosition(position800);
+ const cursorPos = position800.timestamp.getValueNs();
+
+ fixture.detectChanges();
+
+ const zoomButton = htmlElement.querySelector('#zoom-in-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+
+ for (let i = 0; i < 10; i++) {
+ zoomButton.click();
+ fixture.detectChanges();
+ const finalZoom = timelineData.getZoomRange();
+ expect(finalZoom).not.toBe(initialZoom);
+ expect(finalZoom.from.getValueNs()).toBeGreaterThanOrEqual(
+ Number(initialZoom.from.getValueNs())
+ );
+ expect(finalZoom.to.getValueNs()).toBeLessThanOrEqual(Number(initialZoom.to.getValueNs()));
+ expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
+ Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+ );
+
+ // center to get closer to cursor or stay on cursor
+ const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+ const prevCenter = initialZoom.from.plus(initialZoom.to).div(2n).getValueNs();
+
+ if (prevCenter === position800.timestamp.getValueNs()) {
+ expect(curCenter).toBe(prevCenter);
+ } else {
+ expect(Math.abs(Number(curCenter - cursorPos))).toBeLessThan(
+ Math.abs(Number(prevCenter - cursorPos))
+ );
+ }
+
+ initialZoom = finalZoom;
+ }
+ });
+
+ it('can zoom out with the buttons', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ let initialZoom = {
+ from: new RealTimestamp(700n),
+ to: new RealTimestamp(810n),
+ };
+ component.onZoomChanged(initialZoom);
+
+ component.timelineData.setPosition(position800);
+ const cursorPos = position800.timestamp.getValueNs();
+
+ fixture.detectChanges();
+
+ const zoomButton = htmlElement.querySelector('#zoom-out-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+
+ for (let i = 0; i < 10; i++) {
+ zoomButton.click();
+ fixture.detectChanges();
+ const finalZoom = timelineData.getZoomRange();
+ expect(finalZoom).not.toBe(initialZoom);
+ expect(finalZoom.from.getValueNs()).toBeLessThanOrEqual(
+ Number(initialZoom.from.getValueNs())
+ );
+ expect(finalZoom.to.getValueNs()).toBeGreaterThanOrEqual(Number(initialZoom.to.getValueNs()));
+ expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
+ Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+ );
+
+ // center to get closer to cursor or stay on cursor unless we reach the edge
+ const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+ const prevCenter = initialZoom.from.plus(initialZoom.to).div(2n).getValueNs();
+
+ if (
+ finalZoom.from.getValueNs() === timestamp10.getValueNs() ||
+ finalZoom.to.getValueNs() === timestamp1000.getValueNs()
+ ) {
+ // No checks as cursor will stop being more centered
+ } else if (prevCenter === cursorPos) {
+ expect(curCenter).toBe(prevCenter);
+ } else {
+ expect(Math.abs(Number(curCenter - cursorPos))).toBeGreaterThan(
+ Math.abs(Number(prevCenter - cursorPos))
+ );
+ }
+
+ initialZoom = finalZoom;
+ }
+ });
+
+ it('can not zoom out past full range', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ const initialZoom = {
+ from: timestamp10,
+ to: timestamp1000,
+ };
+ component.onZoomChanged(initialZoom);
+
+ component.timelineData.setPosition(position800);
+ const cursorPos = position800.timestamp.getValueNs();
+
+ fixture.detectChanges();
+
+ const zoomButton = htmlElement.querySelector('#zoom-out-btn') as HTMLButtonElement;
+ expect(zoomButton).toBeTruthy();
+
+ zoomButton.click();
+ fixture.detectChanges();
+ const finalZoom = timelineData.getZoomRange();
+
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ });
+
+ it('zooms in with scroll wheel', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ let initialZoom = {
+ from: timestamp10,
+ to: timestamp1000,
+ };
+ component.onZoomChanged(initialZoom);
+
+ fixture.detectChanges();
+
+ for (let i = 0; i < 10; i++) {
+ component.onScroll({
+ deltaY: -200,
+ deltaX: 0,
+ x: 10, // scrolling on pos
+ target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+ } as any as WheelEvent);
+
+ fixture.detectChanges();
+ const finalZoom = timelineData.getZoomRange();
+ expect(finalZoom).not.toBe(initialZoom);
+ expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
+ Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+ );
+
+ initialZoom = finalZoom;
+ }
+ });
+
+ it('zooms out with scroll wheel', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ let initialZoom = {
+ from: new RealTimestamp(700n),
+ to: new RealTimestamp(810n),
+ };
+ component.onZoomChanged(initialZoom);
+
+ fixture.detectChanges();
+
+ for (let i = 0; i < 10; i++) {
+ component.onScroll({
+ deltaY: 200,
+ deltaX: 0,
+ x: 10, // scrolling on pos
+ target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+ } as any as WheelEvent);
+
+ fixture.detectChanges();
+ const finalZoom = timelineData.getZoomRange();
+ expect(finalZoom).not.toBe(initialZoom);
+ expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
+ Number(initialZoom.to.minus(initialZoom.from).getValueNs())
+ );
+
+ initialZoom = finalZoom;
+ }
+ });
+
+ it('cannot zoom out past full range', () => {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
+ .build();
+
+ component.timelineData.initialize(traces, undefined);
+
+ const initialZoom = {
+ from: timestamp10,
+ to: timestamp1000,
+ };
+ component.onZoomChanged(initialZoom);
+
+ component.onScroll({
+ deltaY: 1000,
+ deltaX: 0,
+ x: 10, // scrolling on pos
+ target: {id: 'mini-timeline-canvas', offsetLeft: 0},
+ } as any as WheelEvent);
+
+ fixture.detectChanges();
+
+ const finalZoom = timelineData.getZoomRange();
+
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ });
+});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
new file mode 100644
index 0000000..5badc62
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Inject,
+ Input,
+ Output,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core';
+import {Color} from 'app/colors';
+import {assertDefined} from 'common/assert_utils';
+import {Point} from 'common/geometry_utils';
+import {TimeRange, Timestamp} from 'common/time';
+import {TracePosition} from 'trace/trace_position';
+import {Transformer} from './transformer';
+
+@Component({
+ selector: 'slider',
+ template: `
+ <div id="timeline-slider-box" #sliderBox>
+ <div class="background line"></div>
+ <div
+ class="slider"
+ cdkDragLockAxis="x"
+ cdkDragBoundary="#timeline-slider-box"
+ cdkDrag
+ (cdkDragMoved)="onSliderMove($event)"
+ (cdkDragStarted)="onSlideStart($event)"
+ (cdkDragEnded)="onSlideEnd($event)"
+ [cdkDragFreeDragPosition]="dragPosition"
+ [style]="{width: sliderWidth + 'px'}">
+ <div class="left cropper" (mousedown)="startMoveLeft($event)"></div>
+ <div class="handle" cdkDragHandle></div>
+ <div class="right cropper" (mousedown)="startMoveRight($event)"></div>
+ </div>
+ <div class="cursor" [style]="{left: cursorOffset + 'px'}"></div>
+ </div>
+ `,
+ styles: [
+ `
+ #timeline-slider-box {
+ position: relative;
+ margin: 5px 0;
+ }
+
+ #timeline-slider-box,
+ .slider {
+ height: 10px;
+ }
+
+ .line {
+ height: 3px;
+ position: absolute;
+ margin: auto;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ }
+
+ .background.line {
+ width: 100%;
+ background: ${Color.GUIDE_BAR_LIGHT};
+ }
+
+ .selection.line {
+ background: ${Color.SELECTOR_COLOR};
+ }
+
+ .slider {
+ display: flex;
+ justify-content: space-between;
+ cursor: grab;
+ position: absolute;
+ }
+
+ .handle {
+ flex-grow: 1;
+ background: ${Color.SELECTION_BACKGROUND};
+ cursor: grab;
+ }
+
+ .cropper {
+ width: 5px;
+ background: ${Color.SELECTOR_COLOR};
+ }
+
+ .cropper.left,
+ .cropper.right {
+ cursor: ew-resize;
+ }
+
+ .cursor {
+ width: 2px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: ${Color.ACTIVE_POINTER};
+ }
+ `,
+ ],
+})
+export class SliderComponent {
+ @Input() fullRange: TimeRange | undefined;
+ @Input() zoomRange: TimeRange | undefined;
+ @Input() currentPosition: TracePosition | undefined;
+
+ @Output() onZoomChanged = new EventEmitter<TimeRange>();
+
+ dragging = false;
+ sliderWidth = 0;
+ dragPosition: Point = {x: 0, y: 0};
+ viewInitialized = false;
+ cursorOffset = 0;
+
+ @ViewChild('sliderBox', {static: false}) sliderBox!: ElementRef;
+
+ constructor(@Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef) {}
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['zoomRange'] !== undefined && !this.dragging) {
+ const zoomRange = changes['zoomRange'].currentValue as TimeRange;
+ this.syncDragPositionTo(zoomRange);
+ }
+
+ if (changes['currentPosition']) {
+ const currentPosition = changes['currentPosition'].currentValue as TracePosition;
+ this.syncCursosPositionTo(currentPosition.timestamp);
+ }
+ }
+
+ syncDragPositionTo(zoomRange: TimeRange) {
+ this.sliderWidth = this.computeSliderWidth();
+ const middleOfZoomRange = zoomRange.from.plus(zoomRange.to.minus(zoomRange.from).div(2n));
+
+ this.dragPosition = {
+ // Calculation to account for there being a min width of the slider
+ x: this.getTransformer().transform(middleOfZoomRange) - this.sliderWidth / 2,
+ y: 0,
+ };
+ }
+
+ syncCursosPositionTo(timestamp: Timestamp) {
+ this.cursorOffset = this.getTransformer().transform(timestamp);
+ }
+
+ getTransformer(): Transformer {
+ const width = this.viewInitialized ? this.sliderBox.nativeElement.offsetWidth : 0;
+ return new Transformer(assertDefined(this.fullRange), {from: 0, to: width});
+ }
+
+ ngAfterViewInit(): void {
+ this.viewInitialized = true;
+ }
+
+ ngAfterViewChecked() {
+ assertDefined(this.fullRange);
+ const zoomRange = assertDefined(this.zoomRange);
+ this.syncDragPositionTo(zoomRange);
+ this.cdr.detectChanges();
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event: Event) {
+ this.syncDragPositionTo(assertDefined(this.zoomRange));
+ this.syncCursosPositionTo(assertDefined(this.currentPosition).timestamp);
+ }
+
+ computeSliderWidth() {
+ const transformer = this.getTransformer();
+ let width =
+ transformer.transform(assertDefined(this.zoomRange).to) -
+ transformer.transform(assertDefined(this.zoomRange).from);
+ if (width < MIN_SLIDER_WIDTH) {
+ width = MIN_SLIDER_WIDTH;
+ }
+
+ return width;
+ }
+
+ slideStartX: number | undefined = undefined;
+ onSlideStart(e: any) {
+ this.dragging = true;
+ this.slideStartX = e.source.freeDragPosition.x;
+ document.body.classList.add('inheritCursors');
+ document.body.style.cursor = 'grabbing';
+ }
+
+ onSlideEnd(e: any) {
+ this.dragging = false;
+ this.slideStartX = undefined;
+ this.syncDragPositionTo(assertDefined(this.zoomRange));
+ document.body.classList.remove('inheritCursors');
+ document.body.style.cursor = 'unset';
+ }
+
+ onSliderMove(e: any) {
+ const zoomRange = assertDefined(this.zoomRange);
+ let newX = this.slideStartX + e.distance.x;
+ if (newX < 0) {
+ newX = 0;
+ }
+
+ // Calculation to adjust for min width slider
+ const from = this.getTransformer()
+ .untransform(newX + this.sliderWidth / 2)
+ .minus(zoomRange.to.minus(zoomRange.from).div(2n));
+
+ const to = new Timestamp(
+ assertDefined(this.zoomRange).to.getType(),
+ from.getValueNs() +
+ (assertDefined(this.zoomRange).to.getValueNs() -
+ assertDefined(this.zoomRange).from.getValueNs())
+ );
+
+ this.onZoomChanged.emit({from, to});
+ }
+
+ startMoveLeft(e: any) {
+ e.preventDefault();
+
+ const startPos = e.pageX;
+ const startOffset = this.getTransformer().transform(assertDefined(this.zoomRange).from);
+
+ const listener = (event: any) => {
+ const movedX = event.pageX - startPos;
+ let from = this.getTransformer().untransform(startOffset + movedX);
+ if (from.getValueNs() < assertDefined(this.fullRange).from.getValueNs()) {
+ from = assertDefined(this.fullRange).from;
+ }
+ if (from.getValueNs() > assertDefined(this.zoomRange).to.getValueNs()) {
+ from = assertDefined(this.zoomRange).to;
+ }
+ const to = assertDefined(this.zoomRange).to;
+
+ this.onZoomChanged.emit({from, to});
+ };
+ addEventListener('mousemove', listener);
+
+ const mouseUpListener = () => {
+ removeEventListener('mousemove', listener);
+ removeEventListener('mouseup', mouseUpListener);
+ };
+ addEventListener('mouseup', mouseUpListener);
+ }
+
+ startMoveRight(e: any) {
+ e.preventDefault();
+
+ const startPos = e.pageX;
+ const startOffset = this.getTransformer().transform(assertDefined(this.zoomRange).to);
+
+ const listener = (event: any) => {
+ const movedX = event.pageX - startPos;
+ const from = assertDefined(this.zoomRange).from;
+ let to = this.getTransformer().untransform(startOffset + movedX);
+ if (to.getValueNs() > assertDefined(this.fullRange).to.getValueNs()) {
+ to = assertDefined(this.fullRange).to;
+ }
+ if (to.getValueNs() < assertDefined(this.zoomRange).from.getValueNs()) {
+ to = assertDefined(this.zoomRange).from;
+ }
+
+ this.onZoomChanged.emit({from, to});
+ };
+ addEventListener('mousemove', listener);
+
+ const mouseUpListener = () => {
+ removeEventListener('mousemove', listener);
+ removeEventListener('mouseup', mouseUpListener);
+ };
+ addEventListener('mouseup', mouseUpListener);
+ }
+}
+
+export const MIN_SLIDER_WIDTH = 50;
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
new file mode 100644
index 0000000..69ffd29
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy} from '@angular/core';
+import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MatButtonModule} from '@angular/material/button';
+import {MatFormFieldModule} from '@angular/material/form-field';
+import {MatIconModule} from '@angular/material/icon';
+import {MatInputModule} from '@angular/material/input';
+import {MatSelectModule} from '@angular/material/select';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimeRange} from 'common/time';
+import {dragElement} from 'test/utils';
+import {TracePosition} from 'trace/trace_position';
+import {MIN_SLIDER_WIDTH, SliderComponent} from './slider_component';
+
+describe('SliderComponent', () => {
+ let fixture: ComponentFixture<SliderComponent>;
+ let component: SliderComponent;
+ let htmlElement: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatIconModule,
+ MatSelectModule,
+ MatTooltipModule,
+ ReactiveFormsModule,
+ BrowserAnimationsModule,
+ DragDropModule,
+ ],
+ declarations: [SliderComponent],
+ })
+ .overrideComponent(SliderComponent, {
+ set: {changeDetection: ChangeDetectionStrategy.Default},
+ })
+ .compileComponents();
+ fixture = TestBed.createComponent(SliderComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+
+ component.fullRange = {
+ from: new RealTimestamp(100n),
+ to: new RealTimestamp(200n),
+ };
+ component.zoomRange = {
+ from: new RealTimestamp(125n),
+ to: new RealTimestamp(175n),
+ };
+ component.currentPosition = TracePosition.fromTimestamp(new RealTimestamp(150n));
+
+ fixture.detectChanges();
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('reposition properly on zoom', () => {
+ fixture.detectChanges();
+ component.ngOnChanges({
+ zoomRange: {
+ firstChange: true,
+ isFirstChange: () => true,
+ previousValue: undefined,
+ currentValue: component.zoomRange,
+ },
+ });
+ fixture.detectChanges();
+
+ const sliderWitdth = component.sliderBox.nativeElement.offsetWidth;
+ expect(component.sliderWidth).toBe(sliderWitdth / 2);
+ expect(component.dragPosition.x).toBe(sliderWitdth / 4);
+ });
+
+ it('has min width', () => {
+ component.fullRange = {
+ from: new RealTimestamp(100n),
+ to: new RealTimestamp(200n),
+ };
+ component.zoomRange = {
+ from: new RealTimestamp(125n),
+ to: new RealTimestamp(126n),
+ };
+
+ fixture.detectChanges();
+ component.ngOnChanges({
+ zoomRange: {
+ firstChange: true,
+ isFirstChange: () => true,
+ previousValue: undefined,
+ currentValue: component.zoomRange,
+ },
+ });
+ fixture.detectChanges();
+
+ const sliderWidth = component.sliderBox.nativeElement.offsetWidth;
+ expect(component.sliderWidth).toBe(MIN_SLIDER_WIDTH);
+ expect(component.dragPosition.x).toBe(sliderWidth / 4 - MIN_SLIDER_WIDTH / 2);
+ });
+
+ it('repositions slider on resize', () => {
+ const slider = assertDefined(htmlElement.querySelector('.slider'));
+ const cursor = assertDefined(htmlElement.querySelector('.cursor'));
+
+ fixture.detectChanges();
+
+ const initialSliderXPos = slider.getBoundingClientRect().left;
+ const initialCursorXPos = cursor.getBoundingClientRect().left;
+
+ spyOnProperty(component.sliderBox.nativeElement, 'offsetWidth', 'get').and.returnValue(100);
+ expect(component.sliderBox.nativeElement.offsetWidth).toBe(100);
+
+ htmlElement.style.width = '587px';
+ component.onResize({} as Event);
+ fixture.detectChanges();
+
+ expect(initialSliderXPos).not.toBe(slider.getBoundingClientRect().left);
+ expect(initialCursorXPos).not.toBe(cursor.getBoundingClientRect().left);
+ });
+
+ it('draws current position cursor', () => {
+ fixture.detectChanges();
+ component.ngOnChanges({
+ currentPosition: {
+ firstChange: true,
+ isFirstChange: () => true,
+ previousValue: undefined,
+ currentValue: component.currentPosition,
+ },
+ });
+ fixture.detectChanges();
+
+ const sliderBox = assertDefined(htmlElement.querySelector('#timeline-slider-box'));
+ const cursor = assertDefined(htmlElement.querySelector('.cursor'));
+ const sliderBoxRect = sliderBox.getBoundingClientRect();
+ expect(cursor.getBoundingClientRect().left).toBe(
+ (sliderBoxRect.left + sliderBoxRect.right) / 2
+ );
+ });
+
+ it('moving slider around updates zoom', fakeAsync(async () => {
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const slider = htmlElement.querySelector('.slider .handle');
+ expect(slider).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(slider), 100, 8);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from).not.toBe(initialZoom.from);
+ expect(finalZoom.to).not.toBe(initialZoom.to);
+ expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBe(
+ initialZoom.to.minus(initialZoom.from).getValueNs()
+ );
+ }));
+
+ it('moving slider left pointer around updates zoom', fakeAsync(async () => {
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+ expect(leftCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(leftCropper), 5, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from).not.toBe(initialZoom.from);
+ expect(finalZoom.to).toBe(initialZoom.to);
+ }));
+
+ it('moving slider right pointer around updates zoom', fakeAsync(async () => {
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+ expect(rightCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(rightCropper), 5, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from).toBe(initialZoom.from);
+ expect(finalZoom.to).not.toBe(initialZoom.to);
+ }));
+
+ it('cannot slide left cropper past edges', fakeAsync(() => {
+ component.zoomRange = component.fullRange;
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+ expect(leftCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(leftCropper), -5, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ }));
+
+ it('cannot slide right cropper past edges', fakeAsync(() => {
+ component.zoomRange = component.fullRange;
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+ expect(rightCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(rightCropper), 5, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ }));
+
+ it('cannot slide left cropper past right cropper', fakeAsync(() => {
+ component.zoomRange = {
+ from: new RealTimestamp(125n),
+ to: new RealTimestamp(125n),
+ };
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const leftCropper = htmlElement.querySelector('.slider .cropper.left');
+ expect(leftCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(leftCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(leftCropper), 100, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ }));
+
+ it('cannot slide right cropper past left cropper', fakeAsync(() => {
+ component.zoomRange = {
+ from: new RealTimestamp(125n),
+ to: new RealTimestamp(125n),
+ };
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const rightCropper = htmlElement.querySelector('.slider .cropper.right');
+ expect(rightCropper).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(rightCropper)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(rightCropper), -100, 0);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ }));
+
+ it('cannot move slider past edges', fakeAsync(() => {
+ component.zoomRange = component.fullRange;
+ fixture.detectChanges();
+
+ const initialZoom = assertDefined(component.zoomRange);
+
+ let lastZoomUpdate: TimeRange | undefined = undefined;
+ const zoomChangedSpy = spyOn(component.onZoomChanged, 'emit').and.callFake((zoom) => {
+ lastZoomUpdate = zoom;
+ });
+
+ const slider = htmlElement.querySelector('.slider .handle');
+ expect(slider).toBeTruthy();
+ expect(window.getComputedStyle(assertDefined(slider)).visibility).toBe('visible');
+
+ dragElement(fixture, assertDefined(slider), 100, 8);
+
+ expect(zoomChangedSpy).toHaveBeenCalled();
+
+ const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
+ expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs());
+ expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs());
+ }));
+});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
new file mode 100644
index 0000000..dd5a71c
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {TimeRange, Timestamp, TimestampType} from 'common/time';
+import {Segment} from '../utils';
+
+export class Transformer {
+ private timestampType: TimestampType;
+
+ private fromWidth: bigint;
+ private targetWidth: number;
+
+ private fromOffset: bigint;
+ private toOffset: number;
+
+ constructor(private fromRange: TimeRange, private toRange: Segment) {
+ this.timestampType = fromRange.from.getType();
+
+ this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
+ // Needs to be a whole number to be compatible with bigints
+ this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
+
+ this.fromOffset = this.fromRange.from.getValueNs();
+ // Needs to be a whole number to be compatible with bigints
+ this.toOffset = this.toRange.from;
+ }
+
+ transform(x: Timestamp): number {
+ return (
+ this.toOffset +
+ (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
+ );
+ }
+
+ untransform(x: number): Timestamp {
+ x = Math.round(x);
+ const valueNs =
+ this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
+ return new Timestamp(this.timestampType, valueNs);
+ }
+}
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
new file mode 100644
index 0000000..604560f
--- /dev/null
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {RealTimestamp} from 'common/time';
+import {Transformer} from './transformer';
+
+describe('Transformer', () => {
+ it('can transform', () => {
+ const fromRange = {
+ from: new RealTimestamp(1689763211000000000n),
+ to: new RealTimestamp(1689763571000000000n),
+ };
+ const toRange = {
+ from: 100,
+ to: 1100,
+ };
+ const transformer = new Transformer(fromRange, toRange);
+
+ const rangeStart = fromRange.from.getValueNs();
+ const rangeEnd = fromRange.to.getValueNs();
+ const range = fromRange.to.getValueNs() - fromRange.from.getValueNs();
+
+ expect(transformer.transform(fromRange.from)).toBe(toRange.from);
+ expect(transformer.transform(fromRange.to)).toBe(toRange.to);
+
+ expect(transformer.transform(new RealTimestamp(rangeStart + range / 2n))).toBe(
+ toRange.from + (toRange.to - toRange.from) / 2
+ );
+ expect(transformer.transform(new RealTimestamp(rangeStart + range / 4n))).toBe(
+ toRange.from + (toRange.to - toRange.from) / 4
+ );
+ expect(transformer.transform(new RealTimestamp(rangeStart + range / 20n))).toBe(
+ toRange.from + (toRange.to - toRange.from) / 20
+ );
+
+ expect(transformer.transform(new RealTimestamp(rangeStart - range / 2n))).toBe(
+ toRange.from - (toRange.to - toRange.from) / 2
+ );
+ expect(transformer.transform(new RealTimestamp(rangeEnd + range / 2n))).toBe(
+ toRange.to + (toRange.to - toRange.from) / 2
+ );
+ });
+
+ it('can untransform', () => {
+ const fromRange = {
+ from: new RealTimestamp(1689763211000000000n),
+ to: new RealTimestamp(1689763571000000000n),
+ };
+ const toRange = {
+ from: 100,
+ to: 1100,
+ };
+ const transformer = new Transformer(fromRange, toRange);
+
+ const rangeStart = fromRange.from.getValueNs();
+ const rangeEnd = fromRange.to.getValueNs();
+ const range = fromRange.to.getValueNs() - fromRange.from.getValueNs();
+
+ expect(transformer.untransform(toRange.from).getValueNs()).toBe(fromRange.from.getValueNs());
+ expect(transformer.untransform(toRange.to).getValueNs()).toBe(fromRange.to.getValueNs());
+
+ expect(
+ transformer.untransform(toRange.from + (toRange.to - toRange.from) / 2).getValueNs()
+ ).toBe(rangeStart + range / 2n);
+ expect(
+ transformer.untransform(toRange.from + (toRange.to - toRange.from) / 4).getValueNs()
+ ).toBe(rangeStart + range / 4n);
+ expect(
+ transformer.untransform(toRange.from + (toRange.to - toRange.from) / 20).getValueNs()
+ ).toBe(rangeStart + range / 20n);
+
+ expect(
+ transformer.untransform(toRange.from - (toRange.to - toRange.from) / 2).getValueNs()
+ ).toBe(rangeStart - range / 2n);
+ expect(
+ transformer.untransform(toRange.from + (toRange.to - toRange.from) / 2).getValueNs()
+ ).toBe(rangeStart + range / 2n);
+ });
+});
diff --git a/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts b/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts
deleted file mode 100644
index 14c6b77..0000000
--- a/tools/winscope/src/app/components/timeline/mini_canvas_drawer.ts
+++ /dev/null
@@ -1,430 +0,0 @@
-/*
- * 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.
- */
-
-import {TimeRange} from 'app/timeline_data';
-import {TRACE_INFO} from 'app/trace_info';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {Trace} from 'trace/trace';
-import {Traces} from 'trace/traces';
-import {TraceType} from 'trace/trace_type';
-import {Color} from '../../colors';
-import {CanvasDrawer} from '../canvas/canvas_drawer';
-import {CanvasMouseHandler} from '../canvas/canvas_mouse_handler';
-import {DraggableCanvasObject} from '../canvas/draggable_canvas_object';
-import {Segment} from './utils';
-
-export class MiniCanvasDrawerInput {
- constructor(
- public fullRange: TimeRange,
- public selectedPosition: Timestamp,
- public selection: TimeRange,
- public traces: Traces
- ) {}
-
- transform(mapToRange: Segment): MiniCanvasDrawerData {
- const transformer = new Transformer(this.fullRange, mapToRange);
- return new MiniCanvasDrawerData(
- transformer.transform(this.selectedPosition),
- {
- from: transformer.transform(this.selection.from),
- to: transformer.transform(this.selection.to),
- },
- this.transformTracesTimestamps(transformer),
- transformer
- );
- }
-
- private transformTracesTimestamps(transformer: Transformer): Map<TraceType, number[]> {
- const transformedTraceSegments = new Map<TraceType, number[]>();
-
- this.traces.forEachTrace((trace) => {
- transformedTraceSegments.set(trace.type, this.transformTraceTimestamps(transformer, trace));
- });
-
- return transformedTraceSegments;
- }
-
- private transformTraceTimestamps(transformer: Transformer, trace: Trace<{}>): number[] {
- const result: number[] = [];
-
- trace.forEachTimestamp((timestamp) => {
- result.push(transformer.transform(timestamp));
- });
-
- return result;
- }
-}
-
-export class Transformer {
- private timestampType: TimestampType;
-
- private fromWidth: bigint;
- private targetWidth: number;
-
- private fromOffset: bigint;
- private toOffset: number;
-
- constructor(private fromRange: TimeRange, private toRange: Segment) {
- this.timestampType = fromRange.from.getType();
-
- this.fromWidth = this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
- // Needs to be a whole number to be compatible with bigints
- this.targetWidth = Math.round(this.toRange.to - this.toRange.from);
-
- this.fromOffset = this.fromRange.from.getValueNs();
- // Needs to be a whole number to be compatible with bigints
- this.toOffset = Math.round(this.toRange.from);
- }
-
- transform(x: Timestamp): number {
- return (
- this.toOffset +
- (this.targetWidth * Number(x.getValueNs() - this.fromOffset)) / Number(this.fromWidth)
- );
- }
-
- untransform(x: number): Timestamp {
- x = Math.round(x);
- const valueNs =
- this.fromOffset + (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
- return new Timestamp(this.timestampType, valueNs);
- }
-}
-
-class MiniCanvasDrawerOutput {
- constructor(public selectedPosition: Timestamp, public selection: TimeRange) {}
-}
-
-class MiniCanvasDrawerData {
- constructor(
- public selectedPosition: number,
- public selection: Segment,
- public timelineEntries: Map<TraceType, number[]>,
- public transformer: Transformer
- ) {}
-
- toOutput(): MiniCanvasDrawerOutput {
- return new MiniCanvasDrawerOutput(this.transformer.untransform(this.selectedPosition), {
- from: this.transformer.untransform(this.selection.from),
- to: this.transformer.untransform(this.selection.to),
- });
- }
-}
-
-export class MiniCanvasDrawer implements CanvasDrawer {
- ctx: CanvasRenderingContext2D;
- handler: CanvasMouseHandler;
-
- private activePointer: DraggableCanvasObject;
- private leftFocusSectionSelector: DraggableCanvasObject;
- private rightFocusSectionSelector: DraggableCanvasObject;
-
- private get pointerWidth() {
- return this.getHeight() / 6;
- }
-
- getXScale() {
- return this.ctx.getTransform().m11;
- }
-
- getYScale() {
- return this.ctx.getTransform().m22;
- }
-
- getWidth() {
- return this.canvas.width / this.getXScale();
- }
-
- getHeight() {
- return this.canvas.height / this.getYScale();
- }
-
- get usableRange() {
- return {
- from: this.padding.left,
- to: this.getWidth() - this.padding.left - this.padding.right,
- };
- }
-
- get input() {
- return this.inputGetter().transform(this.usableRange);
- }
-
- constructor(
- public canvas: HTMLCanvasElement,
- private inputGetter: () => MiniCanvasDrawerInput,
- private onPointerPositionDragging: (pos: Timestamp) => void,
- private onPointerPositionChanged: (pos: Timestamp) => void,
- private onSelectionChanged: (selection: TimeRange) => void,
- private onUnhandledClick: (pos: Timestamp) => void
- ) {
- const ctx = canvas.getContext('2d');
-
- if (ctx === null) {
- throw Error('MiniTimeline canvas context was null!');
- }
-
- this.ctx = ctx;
-
- const onUnhandledClickInternal = (x: number, y: number) => {
- this.onUnhandledClick(this.input.transformer.untransform(x));
- };
- this.handler = new CanvasMouseHandler(this, 'pointer', onUnhandledClickInternal);
-
- this.activePointer = new DraggableCanvasObject(
- this,
- () => this.selectedPosition,
- (ctx: CanvasRenderingContext2D, position: number) => {
- const barWidth = 3;
- const triangleHeight = this.pointerWidth / 2;
-
- ctx.beginPath();
- ctx.moveTo(position - triangleHeight, 0);
- ctx.lineTo(position + triangleHeight, 0);
- ctx.lineTo(position + barWidth / 2, triangleHeight);
- ctx.lineTo(position + barWidth / 2, this.getHeight());
- ctx.lineTo(position - barWidth / 2, this.getHeight());
- ctx.lineTo(position - barWidth / 2, triangleHeight);
- ctx.closePath();
- },
- {
- fillStyle: Color.ACTIVE_POINTER,
- fill: true,
- },
- (x) => {
- this.input.selectedPosition = x;
- this.onPointerPositionDragging(this.input.transformer.untransform(x));
- },
- (x) => {
- this.input.selectedPosition = x;
- this.onPointerPositionChanged(this.input.transformer.untransform(x));
- },
- () => this.usableRange
- );
-
- const focusSelectorDrawConfig = {
- fillStyle: Color.SELECTOR_COLOR,
- fill: true,
- };
-
- const onLeftSelectionChanged = (x: number) => {
- this.selection.from = x;
- this.onSelectionChanged({
- from: this.input.transformer.untransform(x),
- to: this.input.transformer.untransform(this.selection.to),
- });
- };
- const onRightSelectionChanged = (x: number) => {
- this.selection.to = x;
- this.onSelectionChanged({
- from: this.input.transformer.untransform(this.selection.from),
- to: this.input.transformer.untransform(x),
- });
- };
-
- const barWidth = 6;
- const selectorArrowWidth = this.innerHeight / 12;
- const selectorArrowHeight = selectorArrowWidth * 2;
-
- this.leftFocusSectionSelector = new DraggableCanvasObject(
- this,
- () => this.selection.from,
- (ctx: CanvasRenderingContext2D, position: number) => {
- ctx.beginPath();
- ctx.moveTo(position - barWidth, this.padding.top);
- ctx.lineTo(position, this.padding.top);
- ctx.lineTo(position + selectorArrowWidth, this.padding.top + selectorArrowWidth);
- ctx.lineTo(position, this.padding.top + selectorArrowHeight);
- ctx.lineTo(position, this.padding.top + this.innerHeight);
- ctx.lineTo(position - barWidth, this.padding.top + this.innerHeight);
- ctx.lineTo(position - barWidth, this.padding.top);
- ctx.closePath();
- },
- focusSelectorDrawConfig,
- onLeftSelectionChanged,
- onLeftSelectionChanged,
- () => {
- return {
- from: this.usableRange.from,
- to: this.rightFocusSectionSelector.position - selectorArrowWidth - barWidth,
- };
- }
- );
-
- this.rightFocusSectionSelector = new DraggableCanvasObject(
- this,
- () => this.selection.to,
- (ctx: CanvasRenderingContext2D, position: number) => {
- ctx.beginPath();
- ctx.moveTo(position + barWidth, this.padding.top);
- ctx.lineTo(position, this.padding.top);
- ctx.lineTo(position - selectorArrowWidth, this.padding.top + selectorArrowWidth);
- ctx.lineTo(position, this.padding.top + selectorArrowHeight);
- ctx.lineTo(position, this.padding.top + this.innerHeight);
- ctx.lineTo(position + barWidth, this.padding.top + this.innerHeight);
- ctx.closePath();
- },
- focusSelectorDrawConfig,
- onRightSelectionChanged,
- onRightSelectionChanged,
- () => {
- return {
- from: this.leftFocusSectionSelector.position + selectorArrowWidth + barWidth,
- to: this.usableRange.to,
- };
- }
- );
- }
-
- get selectedPosition() {
- return this.input.selectedPosition;
- }
-
- get selection() {
- return this.input.selection;
- }
-
- get timelineEntries() {
- return this.input.timelineEntries;
- }
-
- get padding() {
- return {
- top: Math.ceil(this.getHeight() / 5),
- bottom: Math.ceil(this.getHeight() / 5),
- left: Math.ceil(this.pointerWidth / 2),
- right: Math.ceil(this.pointerWidth / 2),
- };
- }
-
- get innerHeight() {
- return this.getHeight() - this.padding.top - this.padding.bottom;
- }
-
- draw() {
- this.ctx.clearRect(0, 0, this.getWidth(), this.getHeight());
-
- this.drawSelectionBackground();
-
- this.drawTraceLines();
-
- this.drawTimelineGuides();
-
- this.leftFocusSectionSelector.draw(this.ctx);
- this.rightFocusSectionSelector.draw(this.ctx);
-
- this.activePointer.draw(this.ctx);
- }
-
- private drawSelectionBackground() {
- const triangleHeight = this.innerHeight / 6;
-
- // Selection background
- this.ctx.globalAlpha = 0.8;
- this.ctx.fillStyle = Color.SELECTION_BACKGROUND;
- const width = this.selection.to - this.selection.from;
- this.ctx.fillRect(
- this.selection.from,
- this.padding.top + triangleHeight / 2,
- width,
- this.innerHeight - triangleHeight / 2
- );
- this.ctx.restore();
- }
-
- private drawTraceLines() {
- const lineHeight = this.innerHeight / 8;
-
- let fromTop = this.padding.top + (this.innerHeight * 2) / 3 - lineHeight;
-
- this.timelineEntries.forEach((entries, traceType) => {
- // TODO: Only if active or a selected trace
- for (const entry of entries) {
- this.ctx.globalAlpha = 0.7;
- this.ctx.fillStyle = TRACE_INFO[traceType].color;
-
- const width = 5;
- this.ctx.fillRect(entry - width / 2, fromTop, width, lineHeight);
- this.ctx.globalAlpha = 1.0;
- }
-
- fromTop -= (lineHeight * 4) / 3;
- });
- }
-
- private drawTimelineGuides() {
- const edgeBarHeight = (this.innerHeight * 1) / 2;
- const edgeBarWidth = 4;
-
- const boldBarHeight = (this.innerHeight * 1) / 5;
- const boldBarWidth = edgeBarWidth;
-
- const lightBarHeight = (this.innerHeight * 1) / 6;
- const lightBarWidth = 2;
-
- const minSpacing = lightBarWidth * 7;
- const barsInSetWidth = 9 * lightBarWidth + boldBarWidth;
- const barSets = Math.floor(
- (this.getWidth() - edgeBarWidth * 2 - minSpacing) / (barsInSetWidth + 10 * minSpacing)
- );
- const bars = barSets * 10;
-
- // Draw start bar
- this.ctx.fillStyle = Color.GUIDE_BAR;
- this.ctx.fillRect(
- 0,
- this.padding.top + this.innerHeight - edgeBarHeight,
- edgeBarWidth,
- edgeBarHeight
- );
-
- // Draw end bar
- this.ctx.fillStyle = Color.GUIDE_BAR;
- this.ctx.fillRect(
- this.getWidth() - edgeBarWidth,
- this.padding.top + this.innerHeight - edgeBarHeight,
- edgeBarWidth,
- edgeBarHeight
- );
-
- const spacing = (this.getWidth() - barSets * barsInSetWidth - edgeBarWidth) / bars;
- let start = edgeBarWidth + spacing;
- for (let i = 1; i < bars; i++) {
- if (i % 10 === 0) {
- // Draw boldbar
- this.ctx.fillStyle = Color.GUIDE_BAR;
- this.ctx.fillRect(
- start,
- this.padding.top + this.innerHeight - boldBarHeight,
- boldBarWidth,
- boldBarHeight
- );
- start += boldBarWidth; // TODO: Shift a bit
- } else {
- // Draw lightbar
- this.ctx.fillStyle = Color.GUIDE_BAR_LIGHT;
- this.ctx.fillRect(
- start,
- this.padding.top + this.innerHeight - lightBarHeight,
- lightBarWidth,
- lightBarHeight
- );
- start += lightBarWidth;
- }
- start += spacing;
- }
- }
-}
diff --git a/tools/winscope/src/app/components/timeline/mini_timeline_component.ts b/tools/winscope/src/app/components/timeline/mini_timeline_component.ts
deleted file mode 100644
index 2f11e1e..0000000
--- a/tools/winscope/src/app/components/timeline/mini_timeline_component.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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.
- */
-
-import {
- Component,
- ElementRef,
- EventEmitter,
- HostListener,
- Input,
- Output,
- SimpleChanges,
- ViewChild,
-} from '@angular/core';
-import {TimelineData} from 'app/timeline_data';
-import {assertDefined} from 'common/assert_utils';
-import {Timestamp} from 'trace/timestamp';
-import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {MiniCanvasDrawer, MiniCanvasDrawerInput} from './mini_canvas_drawer';
-
-@Component({
- selector: 'mini-timeline',
- template: `
- <div id="mini-timeline-wrapper" #miniTimelineWrapper>
- <canvas #canvas></canvas>
- </div>
- `,
- styles: [
- `
- #mini-timeline-wrapper {
- width: 100%;
- min-height: 5em;
- height: 100%;
- }
- `,
- ],
-})
-export class MiniTimelineComponent {
- @Input() timelineData!: TimelineData;
- @Input() currentTracePosition!: TracePosition;
- @Input() selectedTraces!: TraceType[];
-
- @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
- @Output() onSeekTimestampUpdate = new EventEmitter<Timestamp | undefined>();
-
- @ViewChild('miniTimelineWrapper', {static: false}) miniTimelineWrapper!: ElementRef;
- @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
- get canvas(): HTMLCanvasElement {
- return this.canvasRef.nativeElement;
- }
-
- private drawer: MiniCanvasDrawer | undefined = undefined;
-
- ngAfterViewInit(): void {
- this.makeHiPPICanvas();
-
- const updateTimestampCallback = (timestamp: Timestamp) => {
- this.onSeekTimestampUpdate.emit(undefined);
- this.onTracePositionUpdate.emit(TracePosition.fromTimestamp(timestamp));
- };
-
- this.drawer = new MiniCanvasDrawer(
- this.canvas,
- () => this.getMiniCanvasDrawerInput(),
- (position) => {
- const timestampType = this.timelineData.getTimestampType()!;
- this.onSeekTimestampUpdate.emit(position);
- },
- updateTimestampCallback,
- (selection) => {
- const timestampType = this.timelineData.getTimestampType()!;
- this.timelineData.setSelectionTimeRange(selection);
- },
- updateTimestampCallback
- );
- this.drawer.draw();
- }
-
- ngOnChanges(changes: SimpleChanges) {
- if (this.drawer !== undefined) {
- this.drawer.draw();
- }
- }
-
- private getMiniCanvasDrawerInput() {
- return new MiniCanvasDrawerInput(
- this.timelineData.getFullTimeRange(),
- this.currentTracePosition.timestamp,
- this.timelineData.getSelectionTimeRange(),
- this.getTracesToShow()
- );
- }
-
- private getTracesToShow(): Traces {
- const traces = new Traces();
- this.selectedTraces
- .filter((type) => this.timelineData.getTraces().getTrace(type) !== undefined)
- .forEach((type) => {
- traces.setTrace(type, assertDefined(this.timelineData.getTraces().getTrace(type)));
- });
- return traces;
- }
-
- private makeHiPPICanvas() {
- // Reset any size before computing new size to avoid it interfering with size computations
- this.canvas.width = 0;
- this.canvas.height = 0;
- this.canvas.style.width = 'auto';
- this.canvas.style.height = 'auto';
-
- const width = this.miniTimelineWrapper.nativeElement.offsetWidth;
- const height = this.miniTimelineWrapper.nativeElement.offsetHeight;
-
- const HiPPIwidth = window.devicePixelRatio * width;
- const HiPPIheight = window.devicePixelRatio * height;
-
- this.canvas.width = HiPPIwidth;
- this.canvas.height = HiPPIheight;
- this.canvas.style.width = width + 'px';
- this.canvas.style.height = height + 'px';
-
- // ensure all drawing operations are scaled
- if (window.devicePixelRatio !== 1) {
- const context = this.canvas.getContext('2d')!;
- context.scale(window.devicePixelRatio, window.devicePixelRatio);
- }
- }
-
- @HostListener('window:resize', ['$event'])
- onResize(event: Event) {
- this.makeHiPPICanvas();
- this.drawer?.draw();
- }
-}
diff --git a/tools/winscope/src/app/components/timeline/single_timeline_component.ts b/tools/winscope/src/app/components/timeline/single_timeline_component.ts
deleted file mode 100644
index 290d246..0000000
--- a/tools/winscope/src/app/components/timeline/single_timeline_component.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * 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.
- */
-
-import {
- Component,
- ElementRef,
- EventEmitter,
- Input,
- Output,
- SimpleChanges,
- ViewChild,
-} from '@angular/core';
-import {Color} from 'app/colors';
-import {TimeRange} from 'app/timeline_data';
-import {Timestamp} from 'trace/timestamp';
-import {Trace, TraceEntry} from 'trace/trace';
-import {TracePosition} from 'trace/trace_position';
-
-@Component({
- selector: 'single-timeline',
- template: `
- <div class="single-timeline" #wrapper>
- <canvas #canvas></canvas>
- </div>
- `,
- styles: [
- `
- .single-timeline {
- height: 2rem;
- padding: 1rem 0;
- }
- `,
- ],
-})
-export class SingleTimelineComponent {
- @Input() color = '#AF5CF7';
- @Input() trace!: Trace<{}>;
- @Input() selectedEntry: TraceEntry<{}> | undefined = undefined;
- @Input() selectionRange!: TimeRange;
-
- @Output() onTracePositionUpdate = new EventEmitter<TracePosition>();
-
- @ViewChild('canvas', {static: false}) canvasRef!: ElementRef;
- @ViewChild('wrapper', {static: false}) wrapperRef!: ElementRef;
-
- hoveringEntry?: Timestamp;
-
- private viewInitialized = false;
-
- get canvas() {
- return this.canvasRef.nativeElement;
- }
-
- get ctx(): CanvasRenderingContext2D {
- const ctx = this.canvas.getContext('2d');
-
- if (ctx == null) {
- throw Error('Failed to get canvas context!');
- }
-
- return ctx;
- }
-
- ngOnInit() {
- if (!this.trace || !this.selectionRange) {
- throw Error('Not all required inputs have been set');
- }
- }
-
- ngAfterViewInit() {
- // TODO: Clear observer at some point
- new ResizeObserver(() => this.initializeCanvas()).observe(this.wrapperRef.nativeElement);
- this.initializeCanvas();
- }
-
- initializeCanvas() {
- // Reset any size before computing new size to avoid it interfering with size computations
- this.canvas.width = 0;
- this.canvas.height = 0;
- this.canvas.style.width = 'auto';
- this.canvas.style.height = 'auto';
-
- const computedStyle = getComputedStyle(this.wrapperRef.nativeElement);
- const width = this.wrapperRef.nativeElement.offsetWidth;
- const height =
- this.wrapperRef.nativeElement.offsetHeight -
- // tslint:disable-next-line:ban
- parseFloat(computedStyle.paddingTop) -
- // tslint:disable-next-line:ban
- parseFloat(computedStyle.paddingBottom);
-
- const HiPPIwidth = window.devicePixelRatio * width;
- const HiPPIheight = window.devicePixelRatio * height;
-
- this.canvas.width = HiPPIwidth;
- this.canvas.height = HiPPIheight;
- this.canvas.style.width = width + 'px';
- this.canvas.style.height = height + 'px';
-
- // ensure all drawing operations are scaled
- if (window.devicePixelRatio !== 1) {
- const context = this.canvas.getContext('2d')!;
- context.scale(window.devicePixelRatio, window.devicePixelRatio);
- }
-
- this.redraw();
-
- this.canvas.addEventListener('mousemove', (event: MouseEvent) => {
- this.handleMouseMove(event);
- });
- this.canvas.addEventListener('mousedown', (event: MouseEvent) => {
- this.handleMouseDown(event);
- });
- this.canvas.addEventListener('mouseout', (event: MouseEvent) => {
- this.handleMouseOut(event);
- });
-
- this.viewInitialized = true;
- }
-
- ngOnChanges(changes: SimpleChanges) {
- if (this.viewInitialized) {
- this.redraw();
- }
- }
-
- private handleMouseOut(e: MouseEvent) {
- if (this.hoveringEntry) {
- // If undefined there is no current hover effect so no need to clear
- this.redraw();
- }
- this.hoveringEntry = undefined;
- }
-
- getXScale(): number {
- return this.ctx.getTransform().m11;
- }
-
- getYScale(): number {
- return this.ctx.getTransform().m22;
- }
-
- private handleMouseMove(e: MouseEvent) {
- e.preventDefault();
- e.stopPropagation();
- const mouseX = e.offsetX * this.getXScale();
- const mouseY = e.offsetY * this.getYScale();
-
- this.updateCursor(mouseX, mouseY);
- this.drawEntryHover(mouseX, mouseY);
- }
-
- private drawEntryHover(mouseX: number, mouseY: number) {
- const currentHoverEntry = this.getEntryAt(mouseX, mouseY);
- if (this.hoveringEntry === currentHoverEntry) {
- return;
- }
-
- if (this.hoveringEntry) {
- // If null there is no current hover effect so no need to clear
- this.clearCanvas();
- this.drawTimeline();
- }
-
- this.hoveringEntry = currentHoverEntry;
-
- if (!this.hoveringEntry) {
- return;
- }
-
- this.defineEntryPath(this.hoveringEntry);
-
- this.ctx.globalAlpha = 1.0;
- this.ctx.strokeStyle = Color.ACTIVE_BORDER;
- this.ctx.lineWidth = 2;
- this.ctx.save();
- this.ctx.clip();
- this.ctx.lineWidth *= 2;
- this.ctx.fill();
- this.ctx.stroke();
- this.ctx.restore();
- this.ctx.stroke();
-
- this.ctx.restore();
- }
-
- private clearCanvas() {
- // Clear canvas
- this.ctx.clearRect(0, 0, this.getScaledCanvasWidth(), this.getScaledCanvasHeight());
- }
-
- private getEntryAt(mouseX: number, mouseY: number): Timestamp | undefined {
- // TODO: This can be optimized if it's laggy
- for (let i = 0; i < this.trace.lengthEntries; ++i) {
- const timestamp = this.trace.getEntry(i).getTimestamp();
- this.defineEntryPath(timestamp);
- if (this.ctx.isPointInPath(mouseX, mouseY)) {
- this.canvas.style.cursor = 'pointer';
- return timestamp;
- }
- }
- return undefined;
- }
-
- private updateCursor(mouseX: number, mouseY: number) {
- if (this.getEntryAt(mouseX, mouseY)) {
- this.canvas.style.cursor = 'pointer';
- }
- this.canvas.style.cursor = 'auto';
- }
-
- private handleMouseDown(e: MouseEvent) {
- e.preventDefault();
- e.stopPropagation();
- const mouseX = e.offsetX * this.getXScale();
- const mouseY = e.offsetY * this.getYScale();
-
- const clickedTimestamp = this.getEntryAt(mouseX, mouseY);
-
- if (
- clickedTimestamp &&
- clickedTimestamp.getValueNs() !== this.selectedEntry?.getTimestamp().getValueNs()
- ) {
- this.redraw();
- const entry = this.trace.findClosestEntry(clickedTimestamp);
- if (entry) {
- this.selectedEntry = entry;
- this.onTracePositionUpdate.emit(TracePosition.fromTraceEntry(entry));
- }
- }
- }
-
- getScaledCanvasWidth() {
- return Math.floor(this.canvas.width / this.getXScale());
- }
-
- getScaledCanvasHeight() {
- return Math.floor(this.canvas.height / this.getYScale());
- }
-
- get entryWidth() {
- return this.getScaledCanvasHeight();
- }
-
- get availableWidth() {
- return Math.floor(this.getScaledCanvasWidth() - this.entryWidth);
- }
-
- private defineEntryPath(entry: Timestamp, padding = 0) {
- const start = this.selectionRange.from.getValueNs();
- const end = this.selectionRange.to.getValueNs();
-
- const xPos = Number(
- (BigInt(this.availableWidth) * (entry.getValueNs() - start)) / (end - start)
- );
-
- rect(
- this.ctx,
- xPos + padding,
- padding,
- this.entryWidth - 2 * padding,
- this.entryWidth - 2 * padding
- );
- }
-
- private redraw() {
- this.clearCanvas();
- this.drawTimeline();
- }
-
- private drawTimeline() {
- this.trace.forEachTimestamp((entry) => {
- this.drawEntry(entry);
- });
- this.drawSelectedEntry();
- }
-
- private drawEntry(entry: Timestamp) {
- this.ctx.globalAlpha = 0.2;
-
- this.defineEntryPath(entry);
- this.ctx.fillStyle = this.color;
- this.ctx.fill();
-
- this.ctx.restore();
- }
-
- private drawSelectedEntry() {
- if (this.selectedEntry === undefined) {
- return;
- }
-
- this.ctx.globalAlpha = 1.0;
- this.defineEntryPath(this.selectedEntry.getTimestamp(), 1);
- this.ctx.fillStyle = this.color;
- this.ctx.strokeStyle = Color.ACTIVE_BORDER;
- this.ctx.lineWidth = 3;
- this.ctx.stroke();
- this.ctx.fill();
-
- this.ctx.restore();
- }
-}
-
-function rect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
- ctx.beginPath();
- ctx.moveTo(x, y);
- ctx.lineTo(x + w, y);
- ctx.lineTo(x + w, y + h);
- ctx.lineTo(x, y + h);
- ctx.lineTo(x, y);
- ctx.closePath();
-}
diff --git a/tools/winscope/src/app/components/timeline/timeline_component.ts b/tools/winscope/src/app/components/timeline/timeline_component.ts
index 1650d75..7f05b24 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component.ts
@@ -33,16 +33,13 @@
import {assertDefined} from 'common/assert_utils';
import {FunctionUtils} from 'common/function_utils';
import {StringUtils} from 'common/string_utils';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {
- OnTracePositionUpdate,
- TracePositionUpdateEmitter,
-} from 'interfaces/trace_position_update_emitter';
-import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {TracePositionUpdate, WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
+import {EmitEvent, WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {MiniTimelineComponent} from './mini_timeline_component';
+import {TraceType, TraceTypeUtils} from 'trace/trace_type';
@Component({
selector: 'timeline',
@@ -78,9 +75,10 @@
</button>
<form [formGroup]="timestampForm" class="time-selector-form">
<mat-form-field
- class="time-input"
+ class="time-input elapsed"
appearance="fill"
- (change)="humanElapsedTimeInputChange($event)"
+ (keydown.enter)="onKeydownEnterElapsedTimeInputField($event)"
+ (change)="onHumanElapsedTimeInputChange($event)"
*ngIf="!usingRealtime()">
<input
matInput
@@ -88,9 +86,10 @@
[formControl]="selectedElapsedTimeFormControl" />
</mat-form-field>
<mat-form-field
- class="time-input"
+ class="time-input real"
appearance="fill"
- (change)="humanRealTimeInputChanged($event)"
+ (keydown.enter)="onKeydownEnterRealTimeInputField($event)"
+ (change)="onHumanRealTimeInputChange($event)"
*ngIf="usingRealtime()">
<input
matInput
@@ -98,9 +97,10 @@
[formControl]="selectedRealTimeFormControl" />
</mat-form-field>
<mat-form-field
- class="time-input"
+ class="time-input nano"
appearance="fill"
- (change)="nanosecondsInputTimeChange($event)">
+ (keydown.enter)="onKeydownEnterNanosecondsTimeInputField($event)"
+ (change)="onNanosecondsInputTimeChange($event)">
<input matInput name="nsTimeInput" [formControl]="selectedNsFormControl" />
</mat-form-field>
</form>
@@ -115,35 +115,28 @@
</div>
<div id="trace-selector">
<mat-form-field appearance="none">
- <mat-select
- #traceSelector
- [formControl]="selectedTracesFormControl"
- multiple
- (closed)="onTraceSelectionClosed()">
+ <mat-select #traceSelector [formControl]="selectedTracesFormControl" multiple>
<div class="tip">Select up to 2 additional traces to display.</div>
<mat-option
- *ngFor="let trace of availableTraces"
+ *ngFor="let trace of sortedAvailableTraces"
[value]="trace"
[style]="{
color: TRACE_INFO[trace].color,
opacity: isOptionDisabled(trace) ? 0.5 : 1.0
}"
- [disabled]="isOptionDisabled(trace)">
+ [disabled]="isOptionDisabled(trace)"
+ (click)="applyNewTraceSelection()">
<mat-icon>{{ TRACE_INFO[trace].icon }}</mat-icon>
{{ TRACE_INFO[trace].name }}
</mat-option>
<div class="actions">
- <button mat-button color="primary" (click)="traceSelector.close()">Cancel</button>
- <button
- mat-flat-button
- color="primary"
- (click)="applyNewTraceSelection(); traceSelector.close()">
- Apply
+ <button mat-flat-button color="primary" (click)="traceSelector.close()">
+ Done
</button>
</div>
<mat-select-trigger class="shown-selection">
<mat-icon
- *ngFor="let selectedTrace of selectedTraces"
+ *ngFor="let selectedTrace of getSelectedTracesSortedByDisplayOrder()"
[style]="{color: TRACE_INFO[selectedTrace].color}">
{{ TRACE_INFO[selectedTrace].icon }}
</mat-icon>
@@ -173,7 +166,7 @@
</ng-template>
<div *ngIf="!timelineData.hasTimestamps()" class="no-timestamps-msg">
<p class="mat-body-2">No timeline to show!</p>
- <p class="mat-body-1">All loaded traces contain no timestamps!</p>
+ <p class="mat-body-1">All loaded traces contain no timestamps.</p>
</div>
<div
*ngIf="timelineData.hasTimestamps() && !timelineData.hasMoreThanOneDistinctTimestamp()"
@@ -242,6 +235,9 @@
#expanded-timeline {
flex-grow: 1;
}
+ #trace-selector {
+ padding-bottom: 20px;
+ }
#trace-selector .mat-form-field-infix {
width: 50px;
padding: 0 0.75rem 0 0.5rem;
@@ -293,7 +289,7 @@
`,
],
})
-export class TimelineComponent implements TracePositionUpdateEmitter, TracePositionUpdateListener {
+export class TimelineComponent implements WinscopeEventEmitter, WinscopeEventListener {
readonly TOGGLE_BUTTON_CLASS: string = 'button-toggle-expansion';
readonly MAX_SELECTED_TRACES = 3;
@@ -308,9 +304,9 @@
this.internalActiveTrace = types[0];
- if (!this.selectedTraces.includes(this.internalActiveTrace)) {
- this.selectedTraces.push(this.internalActiveTrace);
- }
+ // Even if new active trace already selected, push to array as most recent selection
+ this.selectedTraces = this.selectedTraces.filter((type) => type !== this.internalActiveTrace);
+ this.selectedTraces.push(this.internalActiveTrace);
if (this.selectedTraces.length > this.MAX_SELECTED_TRACES) {
// Maxed capacity so remove oldest selected trace
@@ -321,19 +317,20 @@
this.selectedTraces = [...this.selectedTraces];
this.selectedTracesFormControl.setValue(this.selectedTraces);
}
- internalActiveTrace: TraceType | undefined = undefined;
@Input() timelineData!: TimelineData;
@Input() availableTraces: TraceType[] = [];
@Output() collapsedTimelineSizeChanged = new EventEmitter<number>();
- @ViewChild('miniTimeline') private miniTimelineComponent!: MiniTimelineComponent;
@ViewChild('collapsedTimeline') private collapsedTimelineRef!: ElementRef;
- selectedTraces: TraceType[] = [];
- selectedTracesFormControl = new FormControl();
+ videoUrl: SafeUrl | undefined;
+ internalActiveTrace: TraceType | undefined = undefined;
+ selectedTraces: TraceType[] = [];
+ sortedAvailableTraces: TraceType[] = [];
+ selectedTracesFormControl = new FormControl<TraceType[]>([]);
selectedElapsedTimeFormControl = new FormControl(
'undefined',
Validators.compose([
@@ -357,14 +354,11 @@
selectedRealTime: this.selectedRealTimeFormControl,
selectedNs: this.selectedNsFormControl,
});
-
- videoUrl: SafeUrl | undefined;
+ TRACE_INFO = TRACE_INFO;
+ isInputFormFocused = false;
private expanded = false;
-
- TRACE_INFO = TRACE_INFO;
-
- private onTracePositionUpdateCallback: OnTracePositionUpdate = FunctionUtils.DO_NOTHING_ASYNC;
+ private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor(
@Inject(DomSanitizer) private sanitizer: DomSanitizer,
@@ -382,6 +376,10 @@
URL.createObjectURL(screenRecordingVideo)
);
}
+
+ this.sortedAvailableTraces = this.availableTraces.sort((a, b) =>
+ TraceTypeUtils.compareByDisplayOrder(a, b)
+ ); // to display in fixed order corresponding to viewer tabs
}
ngAfterViewInit() {
@@ -389,8 +387,8 @@
this.collapsedTimelineSizeChanged.emit(height);
}
- setOnTracePositionUpdate(callback: OnTracePositionUpdate) {
- this.onTracePositionUpdateCallback = callback;
+ setEmitEvent(callback: EmitEvent) {
+ this.emitEvent = callback;
}
getVideoCurrentTime() {
@@ -414,8 +412,14 @@
return position;
}
- onTracePositionUpdate(position: TracePosition) {
- this.updateTimeInputValuesToCurrentTimestamp();
+ getSelectedTracesSortedByDisplayOrder(): TraceType[] {
+ return this.selectedTraces.slice().sort((a, b) => TraceTypeUtils.compareByDisplayOrder(a, b));
+ }
+
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async () => {
+ this.updateTimeInputValuesToCurrentTimestamp();
+ });
}
toggleExpand() {
@@ -425,7 +429,7 @@
async updatePosition(position: TracePosition) {
this.timelineData.setPosition(position);
- await this.onTracePositionUpdateCallback(position);
+ await this.emitEvent(new TracePositionUpdate(position));
}
usingRealtime(): boolean {
@@ -434,7 +438,7 @@
updateSeekTimestamp(timestamp: Timestamp | undefined) {
if (timestamp) {
- this.seekTracePosition = TracePosition.fromTimestamp(timestamp);
+ this.seekTracePosition = this.timelineData.makePositionFromActiveTrace(timestamp);
} else {
this.seekTracePosition = undefined;
}
@@ -472,16 +476,37 @@
return false;
}
- onTraceSelectionClosed() {
- this.selectedTracesFormControl.setValue(this.selectedTraces);
+ applyNewTraceSelection() {
+ this.selectedTraces = this.selectedTracesFormControl.value ?? [];
}
- applyNewTraceSelection() {
- this.selectedTraces = this.selectedTracesFormControl.value;
+ @HostListener('document:focusin', ['$event'])
+ handleFocusInEvent(event: FocusEvent) {
+ if (
+ (event.target as HTMLInputElement)?.tagName === 'INPUT' &&
+ (event.target as HTMLInputElement)?.type === 'text'
+ ) {
+ //check if text input field focused
+ this.isInputFormFocused = true;
+ }
+ }
+
+ @HostListener('document:focusout', ['$event'])
+ handleFocusOutEvent(event: FocusEvent) {
+ if (
+ (event.target as HTMLInputElement)?.tagName === 'INPUT' &&
+ (event.target as HTMLInputElement)?.type === 'text'
+ ) {
+ //check if text input field focused
+ this.isInputFormFocused = false;
+ }
}
@HostListener('document:keydown', ['$event'])
async handleKeyboardEvent(event: KeyboardEvent) {
+ if (this.isInputFormFocused) {
+ return;
+ }
if (event.key === 'ArrowLeft') {
await this.moveToPreviousEntry();
} else if (event.key === 'ArrowRight') {
@@ -490,7 +515,7 @@
}
hasPrevEntry(): boolean {
- if (!this.internalActiveTrace) {
+ if (this.internalActiveTrace === undefined) {
return false;
}
if (this.timelineData.getTraces().getTrace(this.internalActiveTrace) === undefined) {
@@ -500,7 +525,7 @@
}
hasNextEntry(): boolean {
- if (!this.internalActiveTrace) {
+ if (this.internalActiveTrace === undefined) {
return false;
}
if (this.timelineData.getTraces().getTrace(this.internalActiveTrace) === undefined) {
@@ -510,44 +535,46 @@
}
async moveToPreviousEntry() {
- if (!this.internalActiveTrace) {
+ if (this.internalActiveTrace === undefined) {
return;
}
this.timelineData.moveToPreviousEntryFor(this.internalActiveTrace);
- await this.onTracePositionUpdateCallback(assertDefined(this.timelineData.getCurrentPosition()));
+ const position = assertDefined(this.timelineData.getCurrentPosition());
+ await this.emitEvent(new TracePositionUpdate(position));
}
async moveToNextEntry() {
- if (!this.internalActiveTrace) {
+ if (this.internalActiveTrace === undefined) {
return;
}
this.timelineData.moveToNextEntryFor(this.internalActiveTrace);
- await this.onTracePositionUpdateCallback(assertDefined(this.timelineData.getCurrentPosition()));
+ const position = assertDefined(this.timelineData.getCurrentPosition());
+ await this.emitEvent(new TracePositionUpdate(position));
}
- async humanElapsedTimeInputChange(event: Event) {
- if (event.type !== 'change') {
+ async onHumanElapsedTimeInputChange(event: Event) {
+ if (event.type !== 'change' || !this.selectedElapsedTimeFormControl.valid) {
return;
}
const target = event.target as HTMLInputElement;
const timestamp = TimeUtils.parseHumanElapsed(target.value);
- await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+ await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
this.updateTimeInputValuesToCurrentTimestamp();
}
- async humanRealTimeInputChanged(event: Event) {
- if (event.type !== 'change') {
+ async onHumanRealTimeInputChange(event: Event) {
+ if (event.type !== 'change' || !this.selectedRealTimeFormControl.valid) {
return;
}
const target = event.target as HTMLInputElement;
const timestamp = TimeUtils.parseHumanReal(target.value);
- await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+ await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
this.updateTimeInputValuesToCurrentTimestamp();
}
- async nanosecondsInputTimeChange(event: Event) {
- if (event.type !== 'change') {
+ async onNanosecondsInputTimeChange(event: Event) {
+ if (event.type !== 'change' || !this.selectedNsFormControl.valid) {
return;
}
const target = event.target as HTMLInputElement;
@@ -556,7 +583,25 @@
this.timelineData.getTimestampType()!,
StringUtils.parseBigIntStrippingUnit(target.value)
);
- await this.updatePosition(TracePosition.fromTimestamp(timestamp));
+ await this.updatePosition(this.timelineData.makePositionFromActiveTrace(timestamp));
this.updateTimeInputValuesToCurrentTimestamp();
}
+
+ onKeydownEnterElapsedTimeInputField(event: KeyboardEvent) {
+ if (this.selectedElapsedTimeFormControl.valid) {
+ (event.target as HTMLInputElement).blur();
+ }
+ }
+
+ onKeydownEnterRealTimeInputField(event: KeyboardEvent) {
+ if (this.selectedRealTimeFormControl.valid) {
+ (event.target as HTMLInputElement).blur();
+ }
+ }
+
+ onKeydownEnterNanosecondsTimeInputField(event: KeyboardEvent) {
+ if (this.selectedNsFormControl.valid) {
+ (event.target as HTMLInputElement).blur();
+ }
+ }
}
diff --git a/tools/winscope/src/app/components/timeline/timeline_component_stub.ts b/tools/winscope/src/app/components/timeline/timeline_component_stub.ts
deleted file mode 100644
index 265e821..0000000
--- a/tools/winscope/src/app/components/timeline/timeline_component_stub.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.
- */
-
-import {
- OnTracePositionUpdate,
- TracePositionUpdateEmitter,
-} from 'interfaces/trace_position_update_emitter';
-import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
-import {TracePosition} from 'trace/trace_position';
-
-export class TimelineComponentStub
- implements TracePositionUpdateEmitter, TracePositionUpdateListener
-{
- setOnTracePositionUpdate(callback: OnTracePositionUpdate) {
- // do nothing
- }
-
- onTracePositionUpdate(position: TracePosition) {
- // do nothing
- }
-}
diff --git a/tools/winscope/src/app/components/timeline/timeline_component_test.ts b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
index 7720b09..09115fd 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import {ChangeDetectionStrategy} from '@angular/core';
+import {DragDropModule} from '@angular/cdk/drag-drop';
+import {ChangeDetectionStrategy, DebugElement} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
@@ -31,25 +32,30 @@
MatDrawerContent,
} from 'app/components/bottomnav/bottom_drawer_component';
import {TimelineData} from 'app/timeline_data';
+import {TRACE_INFO} from 'app/trace_info';
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
import {TracesBuilder} from 'test/unit/traces_builder';
-import {RealTimestamp} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
-import {ExpandedTimelineComponent} from './expanded_timeline_component';
-import {MiniTimelineComponent} from './mini_timeline_component';
-import {SingleTimelineComponent} from './single_timeline_component';
+import {DefaultTimelineRowComponent} from './expanded-timeline/default_timeline_row_component';
+import {ExpandedTimelineComponent} from './expanded-timeline/expanded_timeline_component';
+import {TransitionTimelineComponent} from './expanded-timeline/transition_timeline_component';
+import {MiniTimelineComponent} from './mini-timeline/mini_timeline_component';
+import {SliderComponent} from './mini-timeline/slider_component';
import {TimelineComponent} from './timeline_component';
describe('TimelineComponent', () => {
const time90 = new RealTimestamp(90n);
const time100 = new RealTimestamp(100n);
const time101 = new RealTimestamp(101n);
+ const time105 = new RealTimestamp(105n);
const time110 = new RealTimestamp(110n);
const time112 = new RealTimestamp(112n);
const position90 = TracePosition.fromTimestamp(time90);
const position100 = TracePosition.fromTimestamp(time100);
- const position105 = TracePosition.fromTimestamp(new RealTimestamp(105n));
+ const position105 = TracePosition.fromTimestamp(time105);
const position110 = TracePosition.fromTimestamp(time110);
const position112 = TracePosition.fromTimestamp(time112);
@@ -69,15 +75,18 @@
MatTooltipModule,
ReactiveFormsModule,
BrowserAnimationsModule,
+ DragDropModule,
],
declarations: [
ExpandedTimelineComponent,
- SingleTimelineComponent,
+ DefaultTimelineRowComponent,
MatDrawer,
MatDrawerContainer,
MatDrawerContent,
MiniTimelineComponent,
TimelineComponent,
+ SliderComponent,
+ TransitionTimelineComponent,
],
})
.overrideComponent(TimelineComponent, {
@@ -88,8 +97,7 @@
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
- const timelineData = new TimelineData();
- component.timelineData = timelineData;
+ component.timelineData = new TimelineData();
});
it('can be created', () => {
@@ -103,8 +111,7 @@
component.timelineData.initialize(traces, undefined);
fixture.detectChanges();
- const button = htmlElement.querySelector(`.${component.TOGGLE_BUTTON_CLASS}`);
- expect(button).toBeTruthy();
+ const button = assertDefined(htmlElement.querySelector(`.${component.TOGGLE_BUTTON_CLASS}`));
// initially not expanded
let expandedTimelineElement = fixture.debugElement.query(
@@ -112,11 +119,11 @@
);
expect(expandedTimelineElement).toBeFalsy();
- button!.dispatchEvent(new Event('click'));
+ button.dispatchEvent(new Event('click'));
expandedTimelineElement = fixture.debugElement.query(By.directive(ExpandedTimelineComponent));
expect(expandedTimelineElement).toBeTruthy();
- button!.dispatchEvent(new Event('click'));
+ button.dispatchEvent(new Event('click'));
expandedTimelineElement = fixture.debugElement.query(By.directive(ExpandedTimelineComponent));
expect(expandedTimelineElement).toBeFalsy();
});
@@ -135,9 +142,8 @@
expect(miniTimelineElement).toBeFalsy();
// error message shown
- const errorMessageContainer = htmlElement.querySelector('.no-timestamps-msg');
- expect(errorMessageContainer).toBeTruthy();
- expect(errorMessageContainer!.textContent).toContain('No timeline to show!');
+ const errorMessageContainer = assertDefined(htmlElement.querySelector('.no-timestamps-msg'));
+ expect(errorMessageContainer.textContent).toContain('No timeline to show!');
});
it('handles some empty traces', () => {
@@ -154,6 +160,7 @@
expect(component.internalActiveTrace).toEqual(TraceType.SURFACE_FLINGER);
expect(component.selectedTraces).toEqual([TraceType.SURFACE_FLINGER]);
+ // setting same trace as active does not affect selected traces
component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
expect(component.internalActiveTrace).toEqual(TraceType.SURFACE_FLINGER);
expect(component.selectedTraces).toEqual([TraceType.SURFACE_FLINGER]);
@@ -170,6 +177,7 @@
TraceType.WINDOW_MANAGER,
]);
+ // oldest trace to be selected is replaced (SF)
component.activeViewTraceTypes = [TraceType.PROTO_LOG];
expect(component.internalActiveTrace).toEqual(TraceType.PROTO_LOG);
expect(component.selectedTraces).toEqual([
@@ -177,6 +185,23 @@
TraceType.WINDOW_MANAGER,
TraceType.PROTO_LOG,
]);
+
+ // setting active trace that is already selected causes it to become most recent selection
+ component.activeViewTraceTypes = [TraceType.TRANSACTIONS];
+ expect(component.internalActiveTrace).toEqual(TraceType.TRANSACTIONS);
+ expect(component.selectedTraces).toEqual([
+ TraceType.WINDOW_MANAGER,
+ TraceType.PROTO_LOG,
+ TraceType.TRANSACTIONS,
+ ]);
+
+ component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
+ expect(component.internalActiveTrace).toEqual(TraceType.SURFACE_FLINGER);
+ expect(component.selectedTraces).toEqual([
+ TraceType.PROTO_LOG,
+ TraceType.TRANSACTIONS,
+ TraceType.SURFACE_FLINGER,
+ ]);
});
it('handles undefined active trace input', () => {
@@ -193,20 +218,91 @@
expect(component.selectedTraces).toEqual([TraceType.SURFACE_FLINGER]);
});
- it('next button disabled if no next entry', () => {
- const traces = new TracesBuilder()
- .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
- .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
- .build();
- component.timelineData.initialize(traces, undefined);
- component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
- component.timelineData.setPosition(position100);
+ it('sorts selected traces correctly for display in selection trigger', () => {
+ loadTracesForSelectorTest();
+
+ fixture.whenStable().then(() => {
+ let selectionTriggerIcons = Array.from(
+ htmlElement.querySelectorAll('.mat-select-trigger .mat-icon')
+ );
+ expect(selectionTriggerIcons.length).toEqual(1);
+ expect(selectionTriggerIcons[0].innerHTML).toContain(
+ TRACE_INFO[TraceType.SURFACE_FLINGER].icon
+ );
+
+ component.activeViewTraceTypes = [TraceType.TRANSACTIONS];
+ selectionTriggerIcons = Array.from(
+ htmlElement.querySelectorAll('.mat-select-trigger mat-icon')
+ );
+ expect(selectionTriggerIcons.length).toEqual(2);
+ expect(selectionTriggerIcons[0].innerHTML).toContain(
+ TRACE_INFO[TraceType.SURFACE_FLINGER].icon
+ );
+ expect(selectionTriggerIcons[1].innerHTML).toContain(TRACE_INFO[TraceType.TRANSACTIONS].icon);
+
+ component.activeViewTraceTypes = [TraceType.WINDOW_MANAGER];
+ selectionTriggerIcons = Array.from(
+ htmlElement.querySelectorAll('.mat-select-trigger mat-icon')
+ );
+ expect(selectionTriggerIcons.length).toEqual(3);
+ expect(selectionTriggerIcons[0].innerHTML).toContain(
+ TRACE_INFO[TraceType.SURFACE_FLINGER].icon
+ );
+ expect(selectionTriggerIcons[1].innerHTML).toContain(
+ TRACE_INFO[TraceType.WINDOW_MANAGER].icon
+ );
+ expect(selectionTriggerIcons[2].innerHTML).toContain(TRACE_INFO[TraceType.TRANSACTIONS].icon);
+
+ component.activeViewTraceTypes = [TraceType.PROTO_LOG];
+ selectionTriggerIcons = Array.from(
+ htmlElement.querySelectorAll('.mat-select-trigger mat-icon')
+ );
+ expect(selectionTriggerIcons.length).toEqual(3);
+ expect(selectionTriggerIcons[0].innerHTML).toContain(
+ TRACE_INFO[TraceType.WINDOW_MANAGER].icon
+ );
+ expect(selectionTriggerIcons[1].innerHTML).toContain(TRACE_INFO[TraceType.TRANSACTIONS].icon);
+ expect(selectionTriggerIcons[2].innerHTML).toContain(TRACE_INFO[TraceType.PROTO_LOG].icon);
+ });
+ });
+
+ it('updates trace selection using selector', () => {
+ loadTracesForSelectorTest();
+
+ const selectTrigger = assertDefined(htmlElement.querySelector('.mat-select-trigger'));
+ (selectTrigger as HTMLElement).click();
fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ const matOptions = Array.from(
+ assertDefined(htmlElement.querySelectorAll('mat-select mat-option'))
+ );
+
+ expect((matOptions[0] as HTMLInputElement).value).toEqual(`${TraceType.SURFACE_FLINGER}`);
+ expect((matOptions[0] as HTMLInputElement).disabled).toBeTrue();
+ for (let i = 1; i < 4; i++) {
+ expect((matOptions[i] as HTMLInputElement).disabled).toBeFalse();
+ }
+
+ (matOptions[1] as HTMLElement).click();
+ (matOptions[2] as HTMLElement).click();
+ expect(component.selectedTraces).toEqual([
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.SCREEN_RECORDING,
+ ]);
+
+ expect((matOptions[3] as HTMLInputElement).value).toEqual(`${TraceType.PROTO_LOG}`);
+ expect((matOptions[3] as HTMLInputElement).disabled).toBeTrue();
+ });
+ });
+
+ it('next button disabled if no next entry', () => {
+ loadTraces();
+
expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- const nextEntryButton = fixture.debugElement.query(By.css('#next_entry_button'));
- expect(nextEntryButton).toBeTruthy();
+ const nextEntryButton = assertDefined(fixture.debugElement.query(By.css('#next_entry_button')));
expect(nextEntryButton.nativeElement.getAttribute('disabled')).toBeFalsy();
component.timelineData.setPosition(position90);
@@ -223,18 +319,10 @@
});
it('prev button disabled if no prev entry', () => {
- const builder = new TracesBuilder()
- .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
- .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112]);
- const traces = builder.build();
- component.timelineData.initialize(traces, undefined);
- component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
- component.timelineData.setPosition(position100);
- fixture.detectChanges();
+ loadTraces();
expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- const prevEntryButton = fixture.debugElement.query(By.css('#prev_entry_button'));
- expect(prevEntryButton).toBeTruthy();
+ const prevEntryButton = assertDefined(fixture.debugElement.query(By.css('#prev_entry_button')));
expect(prevEntryButton.nativeElement.getAttribute('disabled')).toBeTruthy();
component.timelineData.setPosition(position90);
@@ -250,48 +338,154 @@
expect(prevEntryButton.nativeElement.getAttribute('disabled')).toBeFalsy();
});
+ it('next button enabled for different active viewers', () => {
+ loadTraces();
+
+ const nextEntryButton = assertDefined(fixture.debugElement.query(By.css('#next_entry_button')));
+ expect(nextEntryButton.nativeElement.getAttribute('disabled')).toBeFalsy();
+
+ component.activeViewTraceTypes = [TraceType.WINDOW_MANAGER];
+ component.internalActiveTrace = TraceType.WINDOW_MANAGER;
+ fixture.detectChanges();
+
+ expect(nextEntryButton.nativeElement.getAttribute('disabled')).toBeFalsy();
+ });
+
it('changes timestamp on next entry button press', () => {
- const traces = new TracesBuilder()
- .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
- .setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
- .build();
- component.timelineData.initialize(traces, undefined);
- component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
- component.timelineData.setPosition(position100);
- fixture.detectChanges();
+ loadTraces();
+
expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- const nextEntryButton = fixture.debugElement.query(By.css('#next_entry_button'));
- expect(nextEntryButton).toBeTruthy();
+ const nextEntryButton = assertDefined(fixture.debugElement.query(By.css('#next_entry_button')));
- component.timelineData.setPosition(position105);
- fixture.detectChanges();
- nextEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+ testCurrentTimestampOnButtonClick(nextEntryButton, position105, 110n);
- component.timelineData.setPosition(position100);
- fixture.detectChanges();
- nextEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+ testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n);
- component.timelineData.setPosition(position90);
- fixture.detectChanges();
- nextEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+ testCurrentTimestampOnButtonClick(nextEntryButton, position90, 100n);
// No change when we are already on the last timestamp of the active trace
- component.timelineData.setPosition(position110);
- fixture.detectChanges();
- nextEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(110n);
+ testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n);
// No change when we are after the last entry of the active trace
- component.timelineData.setPosition(position112);
- fixture.detectChanges();
- nextEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(112n);
+ testCurrentTimestampOnButtonClick(nextEntryButton, position112, 112n);
});
it('changes timestamp on previous entry button press', () => {
+ loadTraces();
+
+ expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+ const prevEntryButton = assertDefined(fixture.debugElement.query(By.css('#prev_entry_button')));
+
+ // In this state we are already on the first entry at timestamp 100, so
+ // there is no entry to move to before and we just don't update the timestamp
+ testCurrentTimestampOnButtonClick(prevEntryButton, position105, 105n);
+
+ testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n);
+
+ // Active entry here should be 110 so moving back means moving to 100.
+ testCurrentTimestampOnButtonClick(prevEntryButton, position112, 100n);
+
+ // No change when we are already on the first timestamp of the active trace
+ testCurrentTimestampOnButtonClick(prevEntryButton, position100, 100n);
+
+ // No change when we are before the first entry of the active trace
+ testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n);
+ });
+
+ //TODO(b/304982982): find a way to test via dom interactions, not calling listener directly
+ it('performs expected action on arrow key press depending on input form focus', () => {
+ loadTraces();
+
+ const spyNextEntry = spyOn(component, 'moveToNextEntry');
+ const spyPrevEntry = spyOn(component, 'moveToPreviousEntry');
+
+ component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'}));
+ expect(spyNextEntry).toHaveBeenCalled();
+
+ const formElement = htmlElement.querySelector('.time-input input');
+ const focusInEvent = new FocusEvent('focusin');
+ Object.defineProperty(focusInEvent, 'target', {value: formElement});
+ component.handleFocusInEvent(focusInEvent);
+ fixture.detectChanges();
+
+ component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
+ fixture.detectChanges();
+ expect(spyPrevEntry).not.toHaveBeenCalled();
+
+ const focusOutEvent = new FocusEvent('focusout');
+ Object.defineProperty(focusOutEvent, 'target', {value: formElement});
+ component.handleFocusOutEvent(focusOutEvent);
+ fixture.detectChanges();
+
+ component.handleKeyboardEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'}));
+ fixture.detectChanges();
+ expect(spyPrevEntry).toHaveBeenCalled();
+ });
+
+ it('updates position based on ns input field', () => {
+ loadTraces();
+
+ expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+ const timeInputField = assertDefined(fixture.debugElement.query(By.css('.time-input.nano')));
+
+ testCurrentTimestampOnTimeInput(timeInputField, position105, '110 ns', 110n);
+
+ testCurrentTimestampOnTimeInput(timeInputField, position100, '110 ns', 110n);
+
+ testCurrentTimestampOnTimeInput(timeInputField, position90, '100 ns', 100n);
+
+ // No change when we are already on the last timestamp of the active trace
+ testCurrentTimestampOnTimeInput(timeInputField, position110, '110 ns', 110n);
+
+ // No change when we are after the last entry of the active trace
+ testCurrentTimestampOnTimeInput(timeInputField, position112, '112 ns', 112n);
+ });
+
+ it('updates position based on real time input field', () => {
+ loadTraces();
+
+ expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+ const timeInputField = assertDefined(fixture.debugElement.query(By.css('.time-input.real')));
+
+ testCurrentTimestampOnTimeInput(
+ timeInputField,
+ position105,
+ '1970-01-01T00:00:00.000000110',
+ 110n
+ );
+
+ testCurrentTimestampOnTimeInput(
+ timeInputField,
+ position100,
+ '1970-01-01T00:00:00.000000110',
+ 110n
+ );
+
+ testCurrentTimestampOnTimeInput(
+ timeInputField,
+ position90,
+ '1970-01-01T00:00:00.000000100',
+ 100n
+ );
+
+ // No change when we are already on the last timestamp of the active trace
+ testCurrentTimestampOnTimeInput(
+ timeInputField,
+ position110,
+ '1970-01-01T00:00:00.000000110',
+ 110n
+ );
+
+ // No change when we are after the last entry of the active trace
+ testCurrentTimestampOnTimeInput(
+ timeInputField,
+ position112,
+ '1970-01-01T00:00:00.000000112',
+ 112n
+ );
+ });
+
+ function loadTraces() {
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
.setTimestamps(TraceType.WINDOW_MANAGER, [time90, time101, time110, time112])
@@ -300,38 +494,50 @@
component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
component.timelineData.setPosition(position100);
fixture.detectChanges();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- const prevEntryButton = fixture.debugElement.query(By.css('#prev_entry_button'));
- expect(prevEntryButton).toBeTruthy();
+ }
- // In this state we are already on the first entry at timestamp 100, so
- // there is no entry to move to before and we just don't update the timestamp
- component.timelineData.setPosition(position105);
+ function loadTracesForSelectorTest() {
+ const traces = new TracesBuilder()
+ .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
+ .setTimestamps(TraceType.WINDOW_MANAGER, [time100, time110])
+ .setTimestamps(TraceType.SCREEN_RECORDING, [time110])
+ .setTimestamps(TraceType.PROTO_LOG, [time100])
+ .build();
+ component.timelineData.initialize(traces, undefined);
+ component.availableTraces = [
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.SCREEN_RECORDING,
+ TraceType.PROTO_LOG,
+ ];
+ component.activeViewTraceTypes = [TraceType.SURFACE_FLINGER];
fixture.detectChanges();
- prevEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(105n);
+ }
- component.timelineData.setPosition(position110);
+ function testCurrentTimestampOnButtonClick(
+ button: DebugElement,
+ pos: TracePosition,
+ expectedNs: bigint
+ ) {
+ component.timelineData.setPosition(pos);
fixture.detectChanges();
- prevEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
+ button.nativeElement.click();
+ expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(expectedNs);
+ }
- // Active entry here should be 110 so moving back means moving to 100.
- component.timelineData.setPosition(position112);
+ function testCurrentTimestampOnTimeInput(
+ inputField: DebugElement,
+ pos: TracePosition,
+ textInput: string,
+ expectedNs: bigint
+ ) {
+ component.timelineData.setPosition(pos);
fixture.detectChanges();
- prevEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- // No change when we are already on the first timestamp of the active trace
- component.timelineData.setPosition(position100);
+ inputField.nativeElement.value = textInput;
+ inputField.nativeElement.dispatchEvent(new Event('change'));
fixture.detectChanges();
- prevEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(100n);
- // No change when we are before the first entry of the active trace
- component.timelineData.setPosition(position90);
- fixture.detectChanges();
- prevEntryButton.nativeElement.click();
- expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(90n);
- });
+ expect(component.timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual(expectedNs);
+ }
});
diff --git a/tools/winscope/src/app/components/trace_view_component.ts b/tools/winscope/src/app/components/trace_view_component.ts
index 7bd3bd8..e2f525a 100644
--- a/tools/winscope/src/app/components/trace_view_component.ts
+++ b/tools/winscope/src/app/components/trace_view_component.ts
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-import {Component, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
+import {Component, ElementRef, Inject, Input} from '@angular/core';
import {TRACE_INFO} from 'app/trace_info';
+import {FunctionUtils} from 'common/function_utils';
import {PersistentStore} from 'common/persistent_store';
+import {TabbedViewSwitched, WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
+import {EmitEvent, WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {View, Viewer, ViewType} from 'viewers/viewer';
interface Tab extends View {
@@ -56,13 +60,6 @@
</p>
</a>
</nav>
- <button
- color="primary"
- mat-button
- class="save-button"
- (click)="downloadTracesButtonClick.emit()">
- Download all traces
- </button>
</div>
<mat-divider></mat-divider>
<div class="trace-view-content"></div>
@@ -102,18 +99,16 @@
`,
],
})
-export class TraceViewComponent {
+export class TraceViewComponent implements WinscopeEventEmitter, WinscopeEventListener {
@Input() viewers!: Viewer[];
@Input() store!: PersistentStore;
- @Output() downloadTracesButtonClick = new EventEmitter<void>();
- @Output() activeViewChanged = new EventEmitter<View>();
TRACE_INFO = TRACE_INFO;
+ tabs: Tab[] = [];
private elementRef: ElementRef;
-
- tabs: Tab[] = [];
private currentActiveTab: undefined | Tab;
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor(@Inject(ElementRef) elementRef: ElementRef) {
this.elementRef = elementRef;
@@ -124,8 +119,25 @@
this.renderViewsOverlay();
}
- onTabClick(tab: Tab) {
- this.showTab(tab);
+ async onTabClick(tab: Tab) {
+ await this.showTab(tab);
+ }
+
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST, async (event) => {
+ const tab = this.tabs.find((tab) => tab.traceType === event.newFocusedViewId);
+ if (tab) {
+ await this.showTab(tab);
+ }
+ });
+ }
+
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ isCurrentActiveTab(tab: Tab) {
+ return tab === this.currentActiveTab;
}
private renderViewsTab() {
@@ -179,7 +191,7 @@
});
}
- private showTab(tab: Tab) {
+ private async showTab(tab: Tab) {
if (this.currentActiveTab) {
this.currentActiveTab.htmlElement.style.display = 'none';
}
@@ -198,10 +210,7 @@
}
this.currentActiveTab = tab;
- this.activeViewChanged.emit(tab);
- }
- isCurrentActiveTab(tab: Tab) {
- return tab === this.currentActiveTab;
+ await this.emitAppEvent(new TabbedViewSwitched(tab));
}
}
diff --git a/tools/winscope/src/app/components/trace_view_component_test.ts b/tools/winscope/src/app/components/trace_view_component_test.ts
index 37be18a..447bf61 100644
--- a/tools/winscope/src/app/components/trace_view_component_test.ts
+++ b/tools/winscope/src/app/components/trace_view_component_test.ts
@@ -18,6 +18,8 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
import {MatDividerModule} from '@angular/material/divider';
+import {TabbedViewSwitchRequest, WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
+import {TraceType} from 'trace/trace_type';
import {ViewerStub} from 'viewers/viewer_stub';
import {TraceViewComponent} from './trace_view_component';
@@ -36,8 +38,8 @@
htmlElement = fixture.nativeElement;
component = fixture.componentInstance;
component.viewers = [
- new ViewerStub('Title0', 'Content0'),
- new ViewerStub('Title1', 'Content1'),
+ new ViewerStub('Title0', 'Content0', [TraceType.SURFACE_FLINGER]),
+ new ViewerStub('Title1', 'Content1', [TraceType.WINDOW_MANAGER]),
];
component.ngOnChanges();
fixture.detectChanges();
@@ -55,17 +57,7 @@
expect(tabs.item(1)!.textContent).toContain('Title1');
});
- it('changes active view on click', () => {
- const getVisibleTabContents = () => {
- const contents: HTMLElement[] = [];
- htmlElement.querySelectorAll('.trace-view-content div').forEach((content) => {
- if ((content as HTMLElement).style.display !== 'none') {
- contents.push(content as HTMLElement);
- }
- });
- return contents;
- };
-
+ it('switches view on click', () => {
const tabButtons = htmlElement.querySelectorAll('.tab');
// Initially tab 0
@@ -75,32 +67,91 @@
expect(visibleTabContents[0].innerHTML).toEqual('Content0');
// Switch to tab 1
- tabButtons[1].dispatchEvent(new Event('click'));
+ (tabButtons[1] as HTMLButtonElement).click();
fixture.detectChanges();
visibleTabContents = getVisibleTabContents();
expect(visibleTabContents.length).toEqual(1);
expect(visibleTabContents[0].innerHTML).toEqual('Content1');
// Switch to tab 0
- tabButtons[0].dispatchEvent(new Event('click'));
+ (tabButtons[0] as HTMLButtonElement).click();
fixture.detectChanges();
visibleTabContents = getVisibleTabContents();
expect(visibleTabContents.length).toEqual(1);
expect(visibleTabContents[0].innerHTML).toEqual('Content0');
});
- it('emits event on download button click', () => {
- const spy = spyOn(component.downloadTracesButtonClick, 'emit');
+ it("emits 'view switched' events", () => {
+ const tabButtons = htmlElement.querySelectorAll('.tab');
- const downloadButton: null | HTMLButtonElement = htmlElement.querySelector('.save-button');
- expect(downloadButton).toBeInstanceOf(HTMLButtonElement);
+ const emitAppEvent = jasmine.createSpy();
+ component.setEmitEvent(emitAppEvent);
- downloadButton?.dispatchEvent(new Event('click'));
- fixture.detectChanges();
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(emitAppEvent).not.toHaveBeenCalled();
- downloadButton?.dispatchEvent(new Event('click'));
- fixture.detectChanges();
- expect(spy).toHaveBeenCalledTimes(2);
+ (tabButtons[1] as HTMLButtonElement).click();
+ expect(emitAppEvent).toHaveBeenCalledTimes(1);
+ expect(emitAppEvent).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ type: WinscopeEventType.TABBED_VIEW_SWITCHED,
+ } as WinscopeEvent)
+ );
+
+ (tabButtons[0] as HTMLButtonElement).click();
+ expect(emitAppEvent).toHaveBeenCalledTimes(2);
+ expect(emitAppEvent).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ type: WinscopeEventType.TABBED_VIEW_SWITCHED,
+ } as WinscopeEvent)
+ );
});
+
+ it("handles 'view switch' requests", async () => {
+ const tabButtons = htmlElement.querySelectorAll('.tab');
+
+ // Initially tab 0
+ let visibleTabContents = getVisibleTabContents();
+ expect(visibleTabContents.length).toEqual(1);
+ expect(visibleTabContents[0].innerHTML).toEqual('Content0');
+
+ // Switch to tab 1
+ await component.onWinscopeEvent(new TabbedViewSwitchRequest(TraceType.WINDOW_MANAGER));
+ fixture.detectChanges();
+ visibleTabContents = getVisibleTabContents();
+ expect(visibleTabContents.length).toEqual(1);
+ expect(visibleTabContents[0].innerHTML).toEqual('Content1');
+
+ // Switch to tab 0
+ await component.onWinscopeEvent(new TabbedViewSwitchRequest(TraceType.SURFACE_FLINGER));
+ fixture.detectChanges();
+ visibleTabContents = getVisibleTabContents();
+ expect(visibleTabContents.length).toEqual(1);
+ expect(visibleTabContents[0].innerHTML).toEqual('Content0');
+ });
+
+ it('emits tab set onChanges', () => {
+ const emitAppEvent = jasmine.createSpy();
+ component.setEmitEvent(emitAppEvent);
+
+ expect(emitAppEvent).not.toHaveBeenCalled();
+
+ component.ngOnChanges();
+
+ expect(emitAppEvent).toHaveBeenCalledTimes(1);
+ expect(emitAppEvent).toHaveBeenCalledWith(
+ jasmine.objectContaining({
+ type: WinscopeEventType.TABBED_VIEW_SWITCHED,
+ } as WinscopeEvent)
+ );
+ });
+
+ const getVisibleTabContents = () => {
+ const contents: HTMLElement[] = [];
+ htmlElement.querySelectorAll('.trace-view-content div').forEach((content) => {
+ if ((content as HTMLElement).style.display !== 'none') {
+ contents.push(content as HTMLElement);
+ }
+ });
+ return contents;
+ };
});
diff --git a/tools/winscope/src/app/components/upload_traces_component.ts b/tools/winscope/src/app/components/upload_traces_component.ts
index 1652ccc..cb70c6f 100644
--- a/tools/winscope/src/app/components/upload_traces_component.ts
+++ b/tools/winscope/src/app/components/upload_traces_component.ts
@@ -24,8 +24,9 @@
} from '@angular/core';
import {TRACE_INFO} from 'app/trace_info';
import {TracePipeline} from 'app/trace_pipeline';
-import {ProgressListener} from 'interfaces/progress_listener';
+import {ProgressListener} from 'messaging/progress_listener';
import {Trace} from 'trace/trace';
+import {TraceTypeUtils} from 'trace/trace_type';
import {LoadProgressComponent} from './load_progress_component';
@Component({
@@ -39,7 +40,7 @@
ref="drop-box"
(dragleave)="onFileDragOut($event)"
(dragover)="onFileDragIn($event)"
- (drop)="onHandleFileDrop($event)"
+ (drop)="onFileDrop($event)"
(click)="fileDropRef.click()">
<input
id="fileDropRef"
@@ -88,6 +89,9 @@
color="primary"
mat-raised-button
class="load-btn"
+ matTooltip="Upload trace with an associated viewer to visualise"
+ [matTooltipDisabled]="hasLoadedFilesWithViewers()"
+ [disabled]="!hasLoadedFilesWithViewers()"
(click)="onViewTracesButtonClick()">
View traces
</button>
@@ -96,7 +100,13 @@
Upload another file
</button>
- <button color="primary" mat-stroked-button (click)="onClearButtonClick()">Clear all</button>
+ <button
+ class="clear-all-btn"
+ color="primary"
+ mat-stroked-button
+ (click)="onClearButtonClick()">
+ Clear all
+ </button>
</div>
</mat-card>
`,
@@ -228,7 +238,7 @@
e.stopPropagation();
}
- onHandleFileDrop(e: DragEvent) {
+ onFileDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
const droppedFiles = e.dataTransfer?.files;
@@ -243,6 +253,17 @@
this.onOperationFinished();
}
+ hasLoadedFilesWithViewers(): boolean {
+ return this.ngZone.run(() => {
+ let hasFilesWithViewers = false;
+ this.tracePipeline.getTraces().forEachTrace((trace) => {
+ if (TraceTypeUtils.isTraceTypeWithViewer(trace.type)) hasFilesWithViewers = true;
+ });
+
+ return hasFilesWithViewers;
+ });
+ }
+
private getInputFiles(event: Event): File[] {
const files: FileList | null = (event?.target as HTMLInputElement)?.files;
if (!files || !files[0]) {
diff --git a/tools/winscope/src/app/components/upload_traces_component_test.ts b/tools/winscope/src/app/components/upload_traces_component_test.ts
index e2d79ad..282ae31 100644
--- a/tools/winscope/src/app/components/upload_traces_component_test.ts
+++ b/tools/winscope/src/app/components/upload_traces_component_test.ts
@@ -15,32 +15,155 @@
*/
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatCardModule} from '@angular/material/card';
+import {MatIconModule} from '@angular/material/icon';
+import {MatListModule} from '@angular/material/list';
+import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {FilesSource} from 'app/files_source';
import {TracePipeline} from 'app/trace_pipeline';
+import {assertDefined} from 'common/assert_utils';
+import {WinscopeErrorListenerStub} from 'messaging/winscope_error_listener_stub';
+import {UnitTestUtils} from 'test/unit/utils';
+import {LoadProgressComponent} from './load_progress_component';
import {UploadTracesComponent} from './upload_traces_component';
describe('UploadTracesComponent', () => {
let fixture: ComponentFixture<UploadTracesComponent>;
let component: UploadTracesComponent;
let htmlElement: HTMLElement;
+ let validSfFile: File;
+ let validWmFile: File;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [MatCardModule, MatSnackBarModule],
+ imports: [
+ MatCardModule,
+ MatSnackBarModule,
+ MatListModule,
+ MatIconModule,
+ MatProgressBarModule,
+ MatTooltipModule,
+ ],
providers: [MatSnackBar],
- declarations: [UploadTracesComponent],
+ declarations: [UploadTracesComponent, LoadProgressComponent],
}).compileComponents();
- });
-
- beforeEach(() => {
fixture = TestBed.createComponent(UploadTracesComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
- const tracePipeline = new TracePipeline();
- component.tracePipeline = tracePipeline;
+ component.tracePipeline = new TracePipeline();
+ validSfFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'
+ );
+ validWmFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/WindowManager.pb'
+ );
+ fixture.detectChanges();
});
it('can be created', () => {
expect(component).toBeTruthy();
});
+
+ it('renders the expected card title', () => {
+ expect(htmlElement.querySelector('.title')?.innerHTML).toContain('Upload Traces');
+ });
+
+ it('handles file upload via drag and drop', () => {
+ const spy = spyOn(component.filesUploaded, 'emit');
+ const dropbox = assertDefined(htmlElement.querySelector('.drop-box'));
+
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(validSfFile);
+ dropbox?.dispatchEvent(new DragEvent('drop', {dataTransfer}));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalledWith(Array.from(dataTransfer.files));
+ });
+
+ it('displays load progress bar', () => {
+ component.isLoadingFiles = true;
+ fixture.detectChanges();
+ assertDefined(htmlElement.querySelector('load-progress'));
+ });
+
+ it('can display uploaded traces', async () => {
+ await loadFiles([validSfFile]);
+ fixture.detectChanges();
+ assertDefined(htmlElement.querySelector('.uploaded-files'));
+ assertDefined(htmlElement.querySelector('.trace-actions-container'));
+ });
+
+ it('can remove one of two uploaded traces', async () => {
+ await loadFiles([validSfFile, validWmFile]);
+ fixture.detectChanges();
+ expect(component.tracePipeline.getTraces().getSize()).toBe(2);
+
+ const spy = spyOn(component, 'onOperationFinished');
+ const removeButton = assertDefined(htmlElement.querySelector('.uploaded-files button'));
+ (removeButton as HTMLButtonElement).click();
+ fixture.detectChanges();
+ assertDefined(htmlElement.querySelector('.uploaded-files'));
+ expect(spy).toHaveBeenCalled();
+ expect(component.tracePipeline.getTraces().getSize()).toBe(1);
+ });
+
+ it('handles removal of the only uploaded trace', async () => {
+ await loadFiles([validSfFile]);
+ fixture.detectChanges();
+
+ const spy = spyOn(component, 'onOperationFinished');
+ const removeButton = assertDefined(htmlElement.querySelector('.uploaded-files button'));
+ (removeButton as HTMLButtonElement).click();
+ fixture.detectChanges();
+ assertDefined(htmlElement.querySelector('.drop-info'));
+ expect(spy).toHaveBeenCalled();
+ expect(component.tracePipeline.getTraces().getSize()).toBe(0);
+ });
+
+ it('can remove all uploaded traces', async () => {
+ await loadFiles([validSfFile, validWmFile]);
+ fixture.detectChanges();
+ expect(component.tracePipeline.getTraces().getSize()).toBe(2);
+
+ const spy = spyOn(component, 'onOperationFinished');
+ const clearAllButton = assertDefined(htmlElement.querySelector('.clear-all-btn'));
+ (clearAllButton as HTMLButtonElement).click();
+ fixture.detectChanges();
+ assertDefined(htmlElement.querySelector('.drop-info'));
+ expect(spy).toHaveBeenCalled();
+ expect(component.tracePipeline.getTraces().getSize()).toBe(0);
+ });
+
+ it('can triggers view traces event', async () => {
+ await loadFiles([validSfFile]);
+ fixture.detectChanges();
+
+ const spy = spyOn(component.viewTracesButtonClick, 'emit');
+ const viewTracesButton = assertDefined(htmlElement.querySelector('.load-btn'));
+ (viewTracesButton as HTMLButtonElement).click();
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('disables view traces button unless files with viewers uploaded', async () => {
+ const validEventlogFile = await UnitTestUtils.getFixtureFile('traces/eventlog.winscope');
+ await loadFiles([validEventlogFile]);
+ fixture.detectChanges();
+
+ const viewTracesButton = assertDefined(htmlElement.querySelector('.load-btn'));
+ expect((viewTracesButton as HTMLButtonElement).disabled).toBeTrue();
+
+ await loadFiles([validSfFile]);
+ fixture.detectChanges();
+ expect((viewTracesButton as HTMLButtonElement).disabled).toBeFalse();
+ });
+
+ async function loadFiles(files: File[]) {
+ await component.tracePipeline.loadFiles(
+ files,
+ FilesSource.TEST,
+ new WinscopeErrorListenerStub(),
+ undefined
+ );
+ }
});
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/app/files_source.ts
similarity index 78%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/app/files_source.ts
index d2b1744..c7e2c68 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/app/files_source.ts
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
-
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export enum FilesSource {
+ TEST = 'test',
+ COLLECTED = 'collected_traces',
+ UPLOADED = 'uploaded_traces',
+ BUGREPORT = 'bugreport',
+ BUGANIZER = 'buganizer',
}
diff --git a/tools/winscope/src/app/loaded_parsers.ts b/tools/winscope/src/app/loaded_parsers.ts
new file mode 100644
index 0000000..74d50d9
--- /dev/null
+++ b/tools/winscope/src/app/loaded_parsers.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {FileUtils} from 'common/file_utils';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
+import {FileAndParser} from 'parsers/file_and_parser';
+import {FileAndParsers} from 'parsers/file_and_parsers';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {TRACE_INFO} from './trace_info';
+
+export class LoadedParsers {
+ private legacyParsers = new Map<TraceType, FileAndParser>();
+ private perfettoParsers = new Map<TraceType, FileAndParser>();
+
+ addParsers(
+ legacyParsers: FileAndParser[],
+ perfettoParsers: FileAndParsers | undefined,
+ errorListener: WinscopeErrorListener
+ ) {
+ this.addLegacyParsers(legacyParsers, errorListener);
+
+ if (perfettoParsers) {
+ this.addPerfettoParsers(perfettoParsers);
+ }
+ }
+
+ getParsers(): Array<Parser<object>> {
+ const fileAndParsers = [...this.legacyParsers.values(), ...this.perfettoParsers.values()];
+ return fileAndParsers.map((fileAndParser) => fileAndParser.parser);
+ }
+
+ remove(type: TraceType) {
+ this.legacyParsers.delete(type);
+ this.perfettoParsers.delete(type);
+ }
+
+ clear() {
+ this.legacyParsers.clear();
+ this.perfettoParsers.clear();
+ }
+
+ async makeZipArchive(): Promise<Blob> {
+ const archiveFiles: File[] = [];
+
+ if (this.perfettoParsers.size > 0) {
+ const file: TraceFile = this.perfettoParsers.values().next().value.file;
+ const filenameInArchive = FileUtils.removeDirFromFileName(file.file.name);
+ const archiveFile = new File([file.file], filenameInArchive);
+ archiveFiles.push(archiveFile);
+ }
+
+ this.legacyParsers.forEach(({file, parser}, traceType) => {
+ const archiveDir =
+ TRACE_INFO[traceType].downloadArchiveDir.length > 0
+ ? TRACE_INFO[traceType].downloadArchiveDir + '/'
+ : '';
+ const filenameInArchive = archiveDir + FileUtils.removeDirFromFileName(file.file.name);
+ const archiveFile = new File([file.file], filenameInArchive);
+ archiveFiles.push(archiveFile);
+ });
+
+ // Remove duplicates because some traces (e.g. view capture) could share the same file
+ const uniqueArchiveFiles = archiveFiles.filter(
+ (file, index, fileList) => fileList.indexOf(file) === index
+ );
+
+ return await FileUtils.createZipArchive(uniqueArchiveFiles);
+ }
+
+ private addLegacyParsers(parsers: FileAndParser[], errorListener: WinscopeErrorListener) {
+ const legacyParsersBeingLoaded = new Map<TraceType, Parser<object>>();
+
+ parsers.forEach((fileAndParser) => {
+ const {parser} = fileAndParser;
+ if (this.shouldUseLegacyParser(parser, legacyParsersBeingLoaded, errorListener)) {
+ legacyParsersBeingLoaded.set(parser.getTraceType(), parser);
+ this.legacyParsers.set(parser.getTraceType(), fileAndParser);
+ this.perfettoParsers.delete(parser.getTraceType());
+ }
+ });
+ }
+
+ private addPerfettoParsers({file, parsers}: FileAndParsers) {
+ // We currently run only one Perfetto TP WebWorker at a time, so Perfetto parsers previously loaded
+ // are now invalid and must be removed (previous WebWorker is not running anymore).
+ this.perfettoParsers.clear();
+
+ parsers.forEach((parser) => {
+ // While transitioning to the Perfetto format, devices might still have old legacy trace files dangling in the
+ // disk that get automatically included into bugreports. Hence, Perfetto parsers must always override legacy ones
+ // so that dangling legacy files are ignored.
+ this.perfettoParsers.set(parser.getTraceType(), new FileAndParser(file, parser));
+ this.legacyParsers.delete(parser.getTraceType());
+ });
+ }
+
+ private shouldUseLegacyParser(
+ newParser: Parser<object>,
+ parsersBeingLoaded: Map<TraceType, Parser<object>>,
+ errorListener: WinscopeErrorListener
+ ): boolean {
+ const oldParser = this.legacyParsers.get(newParser.getTraceType())?.parser;
+ const currParser = parsersBeingLoaded.get(newParser.getTraceType());
+ if (!oldParser && !currParser) {
+ return true;
+ }
+
+ if (oldParser && !currParser) {
+ errorListener.onError(
+ new WinscopeError(
+ WinscopeErrorType.FILE_OVERRIDDEN,
+ oldParser.getDescriptors().join(),
+ oldParser.getTraceType()
+ )
+ );
+ return true;
+ }
+
+ if (currParser && newParser.getLengthEntries() > currParser.getLengthEntries()) {
+ errorListener.onError(
+ new WinscopeError(
+ WinscopeErrorType.FILE_OVERRIDDEN,
+ currParser.getDescriptors().join(),
+ currParser.getTraceType()
+ )
+ );
+ return true;
+ }
+
+ errorListener.onError(
+ new WinscopeError(
+ WinscopeErrorType.FILE_OVERRIDDEN,
+ newParser.getDescriptors().join(),
+ newParser.getTraceType()
+ )
+ );
+ return false;
+ }
+}
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 955dd6c..e8a0469 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -14,56 +14,52 @@
* limitations under the License.
*/
-import {FileUtils, OnFile} from 'common/file_utils';
-import {BuganizerAttachmentsDownloadEmitter} from 'interfaces/buganizer_attachments_download_emitter';
-import {ProgressListener} from 'interfaces/progress_listener';
-import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
-import {RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
-import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
-import {Runnable} from 'interfaces/runnable';
-import {TraceDataListener} from 'interfaces/trace_data_listener';
-import {TracePositionUpdateEmitter} from 'interfaces/trace_position_update_emitter';
-import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
-import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceFile} from 'trace/trace_file';
+import {Timestamp} from 'common/time';
+import {ProgressListener} from 'messaging/progress_listener';
+import {UserNotificationListener} from 'messaging/user_notification_listener';
+import {WinscopeError} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
+import {
+ TracePositionUpdate,
+ ViewersLoaded,
+ ViewersUnloaded,
+ WinscopeEvent,
+ WinscopeEventType,
+} from 'messaging/winscope_event';
+import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
+import {TraceEntry} from 'trace/trace';
import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {View, Viewer} from 'viewers/viewer';
+import {Viewer} from 'viewers/viewer';
import {ViewerFactory} from 'viewers/viewer_factory';
+import {FilesSource} from './files_source';
import {TimelineData} from './timeline_data';
import {TracePipeline} from './trace_pipeline';
-type TimelineComponentInterface = TracePositionUpdateListener & TracePositionUpdateEmitter;
-type CrossToolProtocolInterface = RemoteBugreportReceiver &
- RemoteTimestampReceiver &
- RemoteTimestampSender;
-type AbtChromeExtensionProtocolInterface = BuganizerAttachmentsDownloadEmitter & Runnable;
-
export class Mediator {
- private abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface;
- private crossToolProtocol: CrossToolProtocolInterface;
+ private abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
+ private crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener;
private uploadTracesComponent?: ProgressListener;
private collectTracesComponent?: ProgressListener;
- private timelineComponent?: TimelineComponentInterface;
- private appComponent: TraceDataListener;
+ private traceViewComponent?: WinscopeEventEmitter & WinscopeEventListener;
+ private timelineComponent?: WinscopeEventEmitter & WinscopeEventListener;
+ private appComponent: WinscopeEventListener;
private userNotificationListener: UserNotificationListener;
private storage: Storage;
private tracePipeline: TracePipeline;
private timelineData: TimelineData;
private viewers: Viewer[] = [];
- private isChangingCurrentTimestamp = false;
- private isTraceDataVisualized = false;
+ private areViewersLoaded = false;
private lastRemoteToolTimestampReceived: Timestamp | undefined;
private currentProgressListener?: ProgressListener;
constructor(
tracePipeline: TracePipeline,
timelineData: TimelineData,
- abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface,
- crossToolProtocol: CrossToolProtocolInterface,
- appComponent: TraceDataListener,
+ abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener,
+ crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener,
+ appComponent: WinscopeEventListener,
userNotificationListener: UserNotificationListener,
storage: Storage
) {
@@ -75,130 +71,139 @@
this.userNotificationListener = userNotificationListener;
this.storage = storage;
- this.crossToolProtocol.setOnBugreportReceived(
- async (bugreport: File, timestamp?: Timestamp) => {
- await this.onRemoteBugreportReceived(bugreport, timestamp);
+ this.crossToolProtocol.setEmitEvent(async (event) => {
+ await this.onWinscopeEvent(event);
+ });
+
+ this.abtChromeExtensionProtocol.setEmitEvent(async (event) => {
+ await this.onWinscopeEvent(event);
+ });
+ }
+
+ setUploadTracesComponent(component: ProgressListener | undefined) {
+ this.uploadTracesComponent = component;
+ }
+
+ setCollectTracesComponent(component: ProgressListener | undefined) {
+ this.collectTracesComponent = component;
+ }
+
+ setTraceViewComponent(component: (WinscopeEventEmitter & WinscopeEventListener) | undefined) {
+ this.traceViewComponent = component;
+ this.traceViewComponent?.setEmitEvent(async (event) => {
+ await this.onWinscopeEvent(event);
+ });
+ }
+
+ setTimelineComponent(component: (WinscopeEventEmitter & WinscopeEventListener) | undefined) {
+ this.timelineComponent = component;
+ this.timelineComponent?.setEmitEvent(async (event) => {
+ await this.onWinscopeEvent(event);
+ });
+ }
+
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.APP_INITIALIZED, async (event) => {
+ await this.abtChromeExtensionProtocol.onWinscopeEvent(event);
+ });
+
+ await event.visit(WinscopeEventType.APP_FILES_UPLOADED, async (event) => {
+ this.currentProgressListener = this.uploadTracesComponent;
+ await this.loadFiles(event.files, FilesSource.UPLOADED);
+ });
+
+ await event.visit(WinscopeEventType.APP_FILES_COLLECTED, async (event) => {
+ this.currentProgressListener = this.collectTracesComponent;
+ await this.loadFiles(event.files, FilesSource.COLLECTED);
+ await this.loadViewers();
+ });
+
+ await event.visit(WinscopeEventType.APP_RESET_REQUEST, async () => {
+ await this.resetAppToInitialState();
+ });
+
+ await event.visit(WinscopeEventType.APP_TRACE_VIEW_REQUEST, async () => {
+ await this.loadViewers();
+ });
+
+ await event.visit(WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOAD_START, async () => {
+ await this.resetAppToInitialState();
+ this.currentProgressListener = this.uploadTracesComponent;
+ this.currentProgressListener?.onProgressUpdate('Downloading files...', undefined);
+ });
+
+ await event.visit(WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOADED, async (event) => {
+ await this.processRemoteFilesReceived(event.files, FilesSource.BUGANIZER);
+ });
+
+ await event.visit(WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST, async (event) => {
+ await this.traceViewComponent?.onWinscopeEvent(event);
+ });
+
+ await event.visit(WinscopeEventType.TABBED_VIEW_SWITCHED, async (event) => {
+ await this.appComponent.onWinscopeEvent(event);
+ this.timelineData.setActiveViewTraceTypes(event.newFocusedView.dependencies);
+ await this.propagateTracePosition(this.timelineData.getCurrentPosition(), false);
+ });
+
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.propagateTracePosition(event.position, false);
+ });
+
+ await event.visit(WinscopeEventType.REMOTE_TOOL_BUGREPORT_RECEIVED, async (event) => {
+ await this.processRemoteFilesReceived([event.bugreport], FilesSource.BUGREPORT);
+ if (event.timestamp !== undefined) {
+ await this.processRemoteToolTimestampReceived(event.timestamp);
}
- );
-
- this.crossToolProtocol.setOnTimestampReceived(async (timestamp: Timestamp) => {
- await this.onRemoteTimestampReceived(timestamp);
});
- this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloadStart(() => {
- this.onBuganizerAttachmentsDownloadStart();
- });
-
- this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloaded(
- async (attachments: File[]) => {
- await this.onBuganizerAttachmentsDownloaded(attachments);
- }
- );
- }
-
- setUploadTracesComponent(uploadTracesComponent: ProgressListener | undefined) {
- this.uploadTracesComponent = uploadTracesComponent;
- }
-
- setCollectTracesComponent(collectTracesComponent: ProgressListener | undefined) {
- this.collectTracesComponent = collectTracesComponent;
- }
-
- setTimelineComponent(timelineComponent: TimelineComponentInterface | undefined) {
- this.timelineComponent = timelineComponent;
- this.timelineComponent?.setOnTracePositionUpdate(async (position) => {
- await this.onTimelineTracePositionUpdate(position);
+ await event.visit(WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED, async (event) => {
+ await this.processRemoteToolTimestampReceived(event.timestamp);
});
}
- onWinscopeInitialized() {
- this.abtChromeExtensionProtocol.run();
- }
+ private async loadFiles(files: File[], source: FilesSource) {
+ const errors: WinscopeError[] = [];
+ const errorListener: WinscopeErrorListener = {
+ onError(error: WinscopeError) {
+ errors.push(error);
+ },
+ };
+ await this.tracePipeline.loadFiles(files, source, errorListener, this.currentProgressListener);
- onWinscopeUploadNew() {
- this.resetAppToInitialState();
- }
-
- async onWinscopeFilesUploaded(files: File[]) {
- this.currentProgressListener = this.uploadTracesComponent;
- await this.processFiles(files);
- }
-
- async onWinscopeFilesCollected(files: File[]) {
- this.currentProgressListener = this.collectTracesComponent;
- await this.processFiles(files);
- await this.processLoadedTraceFiles();
- }
-
- async onWinscopeViewTracesRequest() {
- await this.processLoadedTraceFiles();
- }
-
- async onWinscopeActiveViewChanged(view: View) {
- this.timelineData.setActiveViewTraceTypes(view.dependencies);
- await this.propagateTracePosition(this.timelineData.getCurrentPosition());
- }
-
- async onTimelineTracePositionUpdate(position: TracePosition) {
- await this.propagateTracePosition(position);
+ if (errors.length > 0) {
+ this.userNotificationListener.onErrors(errors);
+ }
}
private async propagateTracePosition(
- position?: TracePosition,
- omitCrossToolProtocol: boolean = false
+ position: TracePosition | undefined,
+ omitCrossToolProtocol: boolean
) {
if (!position) {
return;
}
//TODO (b/289478304): update only visible viewers (1 tab viewer + overlay viewers)
- const promises = this.viewers.map((viewer) => {
- return viewer.onTracePositionUpdate(position);
+ const event = new TracePositionUpdate(position);
+ const receivers: WinscopeEventListener[] = [...this.viewers];
+ if (!omitCrossToolProtocol) {
+ receivers.push(this.crossToolProtocol);
+ }
+ if (this.timelineComponent) {
+ receivers.push(this.timelineComponent);
+ }
+
+ const promises = receivers.map((receiver) => {
+ return receiver.onWinscopeEvent(event);
});
await Promise.all(promises);
-
- this.timelineComponent?.onTracePositionUpdate(position);
-
- if (omitCrossToolProtocol) {
- return;
- }
-
- const timestamp = position.timestamp;
- if (timestamp.getType() !== TimestampType.REAL) {
- console.warn(
- 'Cannot propagate timestamp change to remote tool.' +
- ` Remote tool expects timestamp type ${TimestampType.REAL},` +
- ` but Winscope wants to notify timestamp type ${timestamp.getType()}.`
- );
- return;
- }
-
- this.crossToolProtocol.sendTimestamp(timestamp);
}
- private onBuganizerAttachmentsDownloadStart() {
- this.resetAppToInitialState();
- this.currentProgressListener = this.uploadTracesComponent;
- this.currentProgressListener?.onProgressUpdate('Downloading files...', undefined);
- }
-
- private async onBuganizerAttachmentsDownloaded(attachments: File[]) {
- this.currentProgressListener = this.uploadTracesComponent;
- await this.processRemoteFilesReceived(attachments);
- }
-
- private async onRemoteBugreportReceived(bugreport: File, timestamp?: Timestamp) {
- this.currentProgressListener = this.uploadTracesComponent;
- await this.processRemoteFilesReceived([bugreport]);
- if (timestamp !== undefined) {
- await this.onRemoteTimestampReceived(timestamp);
- }
- }
-
- private async onRemoteTimestampReceived(timestamp: Timestamp) {
+ private async processRemoteToolTimestampReceived(timestamp: Timestamp) {
this.lastRemoteToolTimestampReceived = timestamp;
- if (!this.isTraceDataVisualized) {
+ if (!this.areViewersLoaded) {
return; // apply timestamp later when traces are visualized
}
@@ -211,91 +216,97 @@
return;
}
- const position = TracePosition.fromTimestamp(timestamp);
+ const position = this.timelineData.makePositionFromActiveTrace(timestamp);
this.timelineData.setPosition(position);
await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
}
- private async processRemoteFilesReceived(files: File[]) {
- this.resetAppToInitialState();
- await this.processFiles(files);
+ private async processRemoteFilesReceived(files: File[], source: FilesSource) {
+ await this.resetAppToInitialState();
+ this.currentProgressListener = this.uploadTracesComponent;
+ await this.loadFiles(files, source);
}
- private async processFiles(files: File[]) {
- let progressMessage = '';
- const onProgressUpdate = (progressPercentage: number) => {
- this.currentProgressListener?.onProgressUpdate(progressMessage, progressPercentage);
- };
-
- const traceFiles: TraceFile[] = [];
- const onFile: OnFile = (file: File, parentArchive?: File) => {
- traceFiles.push(new TraceFile(file, parentArchive));
- };
-
- progressMessage = 'Unzipping files...';
- this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
- await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate);
-
- progressMessage = 'Parsing files...';
- this.currentProgressListener?.onProgressUpdate(progressMessage, 0);
- const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate);
- this.currentProgressListener?.onOperationFinished();
- this.userNotificationListener?.onParserErrors(parserErrors);
- }
-
- private async processLoadedTraceFiles() {
+ private async loadViewers() {
this.currentProgressListener?.onProgressUpdate('Computing frame mapping...', undefined);
+ // TODO: move this into the ProgressListener
// allow the UI to update before making the main thread very busy
await new Promise<void>((resolve) => setTimeout(resolve, 10));
await this.tracePipeline.buildTraces();
this.currentProgressListener?.onOperationFinished();
+ this.currentProgressListener?.onProgressUpdate('Initializing UI...', undefined);
+
+ // TODO: move this into the ProgressListener
+ // allow the UI to update before making the main thread very busy
+ await new Promise<void>((resolve) => setTimeout(resolve, 10));
+
this.timelineData.initialize(
this.tracePipeline.getTraces(),
await this.tracePipeline.getScreenRecordingVideo()
);
- await this.createViewers();
- this.appComponent.onTraceDataLoaded(this.viewers);
- this.isTraceDataVisualized = true;
- if (this.lastRemoteToolTimestampReceived !== undefined) {
- await this.onRemoteTimestampReceived(this.lastRemoteToolTimestampReceived);
- }
+ this.viewers = new ViewerFactory().createViewers(this.tracePipeline.getTraces(), this.storage);
+ this.viewers.forEach((viewer) =>
+ viewer.setEmitEvent(async (event) => {
+ await this.onWinscopeEvent(event);
+ })
+ );
+
+ await this.appComponent.onWinscopeEvent(new ViewersLoaded(this.viewers));
+
+ // Set initial trace position as soon as UI is created
+ const initialPosition = this.getInitialTracePosition();
+ this.timelineData.setPosition(initialPosition);
+ await this.propagateTracePosition(initialPosition, true);
+
+ this.areViewersLoaded = true;
}
- private async createViewers() {
- const traces = this.tracePipeline.getTraces();
- const traceTypes = new Set<TraceType>();
- traces.forEachTrace((trace) => {
- traceTypes.add(trace.type);
- });
- this.viewers = new ViewerFactory().createViewers(traceTypes, traces, this.storage);
+ private getInitialTracePosition(): TracePosition | undefined {
+ if (
+ this.lastRemoteToolTimestampReceived &&
+ this.timelineData.getTimestampType() === this.lastRemoteToolTimestampReceived.getType()
+ ) {
+ return this.timelineData.makePositionFromActiveTrace(this.lastRemoteToolTimestampReceived);
+ }
- // Set position as soon as the viewers are created
- await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true);
+ const position = this.timelineData.getCurrentPosition();
+ if (position) {
+ return position;
+ }
+
+ // TimelineData might not provide a TracePosition because all the loaded traces are
+ // dumps with invalid timestamps (value zero). In this case let's create a TracePosition
+ // out of any entry from the loaded traces (if available).
+ const firstEntries = this.tracePipeline
+ .getTraces()
+ .mapTrace((trace) => {
+ if (trace.lengthEntries > 0) {
+ return trace.getEntry(0);
+ }
+ return undefined;
+ })
+ .filter((entry) => {
+ return entry !== undefined;
+ }) as Array<TraceEntry<object>>;
+
+ if (firstEntries.length > 0) {
+ return TracePosition.fromTraceEntry(firstEntries[0]);
+ }
+
+ return undefined;
}
- private async executeIgnoringRecursiveTimestampNotifications(op: () => Promise<void>) {
- if (this.isChangingCurrentTimestamp) {
- return;
- }
- this.isChangingCurrentTimestamp = true;
- try {
- await op();
- } finally {
- this.isChangingCurrentTimestamp = false;
- }
- }
-
- private resetAppToInitialState() {
+ private async resetAppToInitialState() {
this.tracePipeline.clear();
this.timelineData.clear();
this.viewers = [];
- this.isTraceDataVisualized = false;
+ this.areViewersLoaded = false;
this.lastRemoteToolTimestampReceived = undefined;
- this.appComponent.onTraceDataUnloaded();
+ await this.appComponent.onWinscopeEvent(new ViewersUnloaded());
}
}
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts
index 6c681b4..0b63ae4 100644
--- a/tools/winscope/src/app/mediator_test.ts
+++ b/tools/winscope/src/app/mediator_test.ts
@@ -14,19 +14,36 @@
* limitations under the License.
*/
-import {AbtChromeExtensionProtocolStub} from 'abt_chrome_extension/abt_chrome_extension_protocol_stub';
-import {CrossToolProtocolStub} from 'cross_tool/cross_tool_protocol_stub';
-import {ProgressListenerStub} from 'interfaces/progress_listener_stub';
+import {FunctionUtils} from 'common/function_utils';
+import {RealTimestamp} from 'common/time';
+import {ProgressListenerStub} from 'messaging/progress_listener_stub';
+import {UserNotificationListener} from 'messaging/user_notification_listener';
+import {UserNotificationListenerStub} from 'messaging/user_notification_listener_stub';
+import {
+ AppFilesCollected,
+ AppFilesUploaded,
+ AppInitialized,
+ AppTraceViewRequest,
+ BuganizerAttachmentsDownloaded,
+ BuganizerAttachmentsDownloadStart,
+ RemoteToolTimestampReceived,
+ TabbedViewSwitched,
+ TabbedViewSwitchRequest,
+ TracePositionUpdate,
+ ViewersLoaded,
+ WinscopeEvent,
+ WinscopeEventType,
+} from 'messaging/winscope_event';
+import {WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventEmitterStub} from 'messaging/winscope_event_emitter_stub';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
+import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub';
import {MockStorage} from 'test/unit/mock_storage';
import {UnitTestUtils} from 'test/unit/utils';
-import {RealTimestamp} from 'trace/timestamp';
-import {TraceFile} from 'trace/trace_file';
import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
import {ViewerFactory} from 'viewers/viewer_factory';
import {ViewerStub} from 'viewers/viewer_stub';
-import {AppComponentStub} from './components/app_component_stub';
-import {SnackBarOpenerStub} from './components/snack_bar_opener_stub';
-import {TimelineComponentStub} from './components/timeline/timeline_component_stub';
import {Mediator} from './mediator';
import {TimelineData} from './timeline_data';
import {TracePipeline} from './trace_pipeline';
@@ -34,19 +51,22 @@
describe('Mediator', () => {
const viewerStub = new ViewerStub('Title');
let inputFiles: File[];
+ let userNotificationListener: UserNotificationListener;
let tracePipeline: TracePipeline;
let timelineData: TimelineData;
- let abtChromeExtensionProtocol: AbtChromeExtensionProtocolStub;
- let crossToolProtocol: CrossToolProtocolStub;
- let appComponent: AppComponentStub;
- let timelineComponent: TimelineComponentStub;
+ let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
+ let crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener;
+ let appComponent: WinscopeEventListener;
+ let timelineComponent: WinscopeEventEmitter & WinscopeEventListener;
let uploadTracesComponent: ProgressListenerStub;
let collectTracesComponent: ProgressListenerStub;
- let snackBarOpener: SnackBarOpenerStub;
+ let traceViewComponent: WinscopeEventEmitter & WinscopeEventListener;
let mediator: Mediator;
+ let spies: Array<jasmine.Spy<any>>;
const TIMESTAMP_10 = new RealTimestamp(10n);
const TIMESTAMP_11 = new RealTimestamp(11n);
+
const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
@@ -61,88 +81,116 @@
});
beforeEach(async () => {
+ userNotificationListener = new UserNotificationListenerStub();
tracePipeline = new TracePipeline();
timelineData = new TimelineData();
- abtChromeExtensionProtocol = new AbtChromeExtensionProtocolStub();
- crossToolProtocol = new CrossToolProtocolStub();
- appComponent = new AppComponentStub();
- timelineComponent = new TimelineComponentStub();
+ abtChromeExtensionProtocol = FunctionUtils.mixin(
+ new WinscopeEventEmitterStub(),
+ new WinscopeEventListenerStub()
+ );
+ crossToolProtocol = FunctionUtils.mixin(
+ new WinscopeEventEmitterStub(),
+ new WinscopeEventListenerStub()
+ );
+ appComponent = new WinscopeEventListenerStub();
+ timelineComponent = FunctionUtils.mixin(
+ new WinscopeEventEmitterStub(),
+ new WinscopeEventListenerStub()
+ );
uploadTracesComponent = new ProgressListenerStub();
collectTracesComponent = new ProgressListenerStub();
- snackBarOpener = new SnackBarOpenerStub();
+ traceViewComponent = FunctionUtils.mixin(
+ new WinscopeEventEmitterStub(),
+ new WinscopeEventListenerStub()
+ );
mediator = new Mediator(
tracePipeline,
timelineData,
abtChromeExtensionProtocol,
crossToolProtocol,
appComponent,
- snackBarOpener,
+ userNotificationListener,
new MockStorage()
);
mediator.setTimelineComponent(timelineComponent);
mediator.setUploadTracesComponent(uploadTracesComponent);
mediator.setCollectTracesComponent(collectTracesComponent);
+ mediator.setTraceViewComponent(traceViewComponent);
spyOn(ViewerFactory.prototype, 'createViewers').and.returnValue([viewerStub]);
+
+ spies = [
+ spyOn(abtChromeExtensionProtocol, 'onWinscopeEvent'),
+ spyOn(appComponent, 'onWinscopeEvent'),
+ spyOn(collectTracesComponent, 'onOperationFinished'),
+ spyOn(collectTracesComponent, 'onProgressUpdate'),
+ spyOn(crossToolProtocol, 'onWinscopeEvent'),
+ spyOn(timelineComponent, 'onWinscopeEvent'),
+ spyOn(timelineData, 'initialize').and.callThrough(),
+ spyOn(traceViewComponent, 'onWinscopeEvent'),
+ spyOn(uploadTracesComponent, 'onProgressUpdate'),
+ spyOn(uploadTracesComponent, 'onOperationFinished'),
+ spyOn(userNotificationListener, 'onErrors'),
+ spyOn(viewerStub, 'onWinscopeEvent'),
+ ];
+ });
+
+ it('notifies ABT chrome extension about app initialization', async () => {
+ expect(abtChromeExtensionProtocol.onWinscopeEvent).not.toHaveBeenCalled();
+
+ await mediator.onWinscopeEvent(new AppInitialized());
+ expect(abtChromeExtensionProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new AppInitialized()
+ );
});
it('handles uploaded traces from Winscope', async () => {
- const spies = [
- spyOn(uploadTracesComponent, 'onProgressUpdate'),
- spyOn(uploadTracesComponent, 'onOperationFinished'),
- spyOn(timelineData, 'initialize').and.callThrough(),
- spyOn(appComponent, 'onTraceDataLoaded'),
- spyOn(viewerStub, 'onTracePositionUpdate'),
- spyOn(timelineComponent, 'onTracePositionUpdate'),
- spyOn(crossToolProtocol, 'sendTimestamp'),
- ];
-
- await mediator.onWinscopeFilesUploaded(inputFiles);
+ await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled();
expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
expect(timelineData.initialize).not.toHaveBeenCalled();
- expect(appComponent.onTraceDataLoaded).not.toHaveBeenCalled();
- expect(viewerStub.onTracePositionUpdate).not.toHaveBeenCalled();
+ expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
+ expect(viewerStub.onWinscopeEvent).not.toHaveBeenCalled();
- spies.forEach((spy) => {
- spy.calls.reset();
- });
- await mediator.onWinscopeViewTracesRequest();
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled();
expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled();
expect(timelineData.initialize).toHaveBeenCalledTimes(1);
- expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]);
+ expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(new ViewersLoaded([viewerStub]));
// propagates trace position on viewers creation
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ jasmine.objectContaining({
+ type: WinscopeEventType.TRACE_POSITION_UPDATE,
+ } as WinscopeEvent)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ jasmine.objectContaining({
+ type: WinscopeEventType.TRACE_POSITION_UPDATE,
+ } as WinscopeEvent)
+ );
+ expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledTimes(0);
});
it('handles collected traces from Winscope', async () => {
- const spies = [
- spyOn(collectTracesComponent, 'onProgressUpdate'),
- spyOn(collectTracesComponent, 'onOperationFinished'),
- spyOn(timelineData, 'initialize').and.callThrough(),
- spyOn(appComponent, 'onTraceDataLoaded'),
- spyOn(viewerStub, 'onTracePositionUpdate'),
- spyOn(timelineComponent, 'onTracePositionUpdate'),
- spyOn(crossToolProtocol, 'sendTimestamp'),
- ];
-
- await mediator.onWinscopeFilesCollected(inputFiles);
+ await mediator.onWinscopeEvent(new AppFilesCollected(inputFiles));
expect(collectTracesComponent.onProgressUpdate).toHaveBeenCalled();
expect(collectTracesComponent.onOperationFinished).toHaveBeenCalled();
expect(timelineData.initialize).toHaveBeenCalledTimes(1);
- expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]);
+ expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(new ViewersLoaded([viewerStub]));
// propagates trace position on viewers creation
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ makeExpectedEvent(WinscopeEventType.TRACE_POSITION_UPDATE)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ makeExpectedEvent(WinscopeEventType.TRACE_POSITION_UPDATE)
+ );
+ expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledTimes(0);
});
//TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with
@@ -151,103 +199,152 @@
//TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js
// (b/262269229).
- it('handles start download event from ABT chrome extension', () => {
- spyOn(uploadTracesComponent, 'onProgressUpdate');
+ it('handles start download event from ABT chrome extension', async () => {
expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0);
- abtChromeExtensionProtocol.onBuganizerAttachmentsDownloadStart();
+ await mediator.onWinscopeEvent(new BuganizerAttachmentsDownloadStart());
expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1);
});
it('handles empty downloaded files from ABT chrome extension', async () => {
- spyOn(uploadTracesComponent, 'onOperationFinished');
expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0);
// Pass files even if empty so that the upload component will update the progress bar
// and display error messages
- await abtChromeExtensionProtocol.onBuganizerAttachmentsDownloaded([]);
+ await mediator.onWinscopeEvent(new BuganizerAttachmentsDownloaded([]));
expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1);
});
- it('propagates trace position update from timeline component', async () => {
- await loadTraceFiles();
- await mediator.onWinscopeViewTracesRequest();
-
- spyOn(viewerStub, 'onTracePositionUpdate');
- spyOn(timelineComponent, 'onTracePositionUpdate');
- spyOn(crossToolProtocol, 'sendTimestamp');
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
+ it('propagates trace position update', async () => {
+ await loadFiles();
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
// notify position
- await mediator.onTimelineTracePositionUpdate(POSITION_10);
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1);
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_10));
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
+ expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
// notify position
- await mediator.onTimelineTracePositionUpdate(POSITION_11);
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(2);
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new TracePositionUpdate(POSITION_11));
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
+ expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
+ });
+
+ it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
+ const dumpFile = await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb');
+ await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
+
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ makeExpectedEvent(WinscopeEventType.TRACE_POSITION_UPDATE)
+ );
});
describe('timestamp received from remote tool', () => {
it('propagates trace position update', async () => {
- await loadTraceFiles();
- await mediator.onWinscopeViewTracesRequest();
-
- spyOn(viewerStub, 'onTracePositionUpdate');
- spyOn(timelineComponent, 'onTracePositionUpdate');
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
+ await loadFiles();
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
// receive timestamp
- await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(TIMESTAMP_10));
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
// receive timestamp
- await crossToolProtocol.onTimestampReceived(TIMESTAMP_11);
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(TIMESTAMP_11));
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
});
it("doesn't propagate timestamp back to remote tool", async () => {
- await loadTraceFiles();
- await mediator.onWinscopeViewTracesRequest();
-
- spyOn(viewerStub, 'onTracePositionUpdate');
- spyOn(crossToolProtocol, 'sendTimestamp');
+ await loadFiles();
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
// receive timestamp
- await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
- expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
- expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
+ resetSpyCalls();
+ await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(TIMESTAMP_10));
+ expect(viewerStub.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_10)
+ );
+ expect(crossToolProtocol.onWinscopeEvent).toHaveBeenCalledTimes(0);
});
- it('defers propagation till traces are loaded and visualized', async () => {
- spyOn(timelineComponent, 'onTracePositionUpdate');
-
+ it('defers trace position propagation till traces are loaded and visualized', async () => {
// keep timestamp for later
- await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
+ await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(TIMESTAMP_10));
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledTimes(0);
// keep timestamp for later (replace previous one)
- await crossToolProtocol.onTimestampReceived(TIMESTAMP_11);
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
+ await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(TIMESTAMP_11));
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledTimes(0);
// apply timestamp
- await loadTraceFiles();
- await mediator.onWinscopeViewTracesRequest();
- expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledWith(POSITION_11);
+ await loadFiles();
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
+ expect(timelineComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TracePositionUpdate(POSITION_11)
+ );
});
});
- const loadTraceFiles = async () => {
- const traceFiles = inputFiles.map((file) => new TraceFile(file));
- const errors = await tracePipeline.loadTraceFiles(traceFiles);
- expect(errors).toEqual([]);
- };
+ it("forwards 'switched view' events", async () => {
+ const view = viewerStub.getViews()[0];
+ expect(appComponent.onWinscopeEvent).not.toHaveBeenCalled();
+
+ await mediator.onWinscopeEvent(new TabbedViewSwitched(view));
+ expect(appComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(new TabbedViewSwitched(view));
+ });
+
+ it("forwards 'switch view' requests from viewers to trace view component", async () => {
+ await mediator.onWinscopeEvent(new AppTraceViewRequest());
+ expect(traceViewComponent.onWinscopeEvent).not.toHaveBeenCalled();
+
+ await viewerStub.emitAppEventForTesting(new TabbedViewSwitchRequest(TraceType.VIEW_CAPTURE));
+ expect(traceViewComponent.onWinscopeEvent).toHaveBeenCalledOnceWith(
+ new TabbedViewSwitchRequest(TraceType.VIEW_CAPTURE)
+ );
+ });
+
+ async function loadFiles() {
+ await mediator.onWinscopeEvent(new AppFilesUploaded(inputFiles));
+ expect(userNotificationListener.onErrors).not.toHaveBeenCalled();
+ }
+
+ function resetSpyCalls() {
+ spies.forEach((spy) => {
+ spy.calls.reset();
+ });
+ }
+
+ function makeExpectedEvent(type: WinscopeEventType): jasmine.ObjectContaining<any> {
+ return jasmine.objectContaining({
+ type,
+ } as WinscopeEvent);
+ }
});
diff --git a/tools/winscope/src/app/timeline_data.ts b/tools/winscope/src/app/timeline_data.ts
index 7910600..2e93477 100644
--- a/tools/winscope/src/app/timeline_data.ts
+++ b/tools/winscope/src/app/timeline_data.ts
@@ -15,20 +15,16 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {TimeRange, Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceEntry} from 'trace/trace';
+import {Trace, TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
-export interface TimeRange {
- from: Timestamp;
- to: Timestamp;
-}
-const INVALID_TIMESTAMP = 0n
+const INVALID_TIMESTAMP = 0n;
export class TimelineData {
private traces = new Traces();
@@ -38,6 +34,7 @@
private lastEntry?: TraceEntry<{}>;
private explicitlySetPosition?: TracePosition;
private explicitlySetSelection?: TimeRange;
+ private explicitlySetZoomRange?: TimeRange;
private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view
initialize(traces: Traces, screenRecordingVideo: Blob | undefined) {
@@ -45,14 +42,12 @@
this.traces = new Traces();
traces.forEachTrace((trace, type) => {
- if (type === TraceType.WINDOW_MANAGER) {
- // Filter out WindowManager dumps with no timestamp from timeline
- if (
- trace.lengthEntries === 1 &&
- trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP
- ) {
- return;
- }
+ // Filter out dumps with invalid timestamp (would mess up the timeline)
+ if (
+ trace.lengthEntries === 1 &&
+ trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP
+ ) {
+ return;
}
this.traces.setTrace(type, trace);
@@ -64,18 +59,31 @@
this.timestampType = this.firstEntry?.getTimestamp().getType();
}
+ private lastReturnedCurrentPosition?: TracePosition;
getCurrentPosition(): TracePosition | undefined {
if (this.explicitlySetPosition) {
return this.explicitlySetPosition;
}
+
+ let currentPosition: TracePosition | undefined = undefined;
+ if (this.firstEntry) {
+ currentPosition = TracePosition.fromTraceEntry(this.firstEntry);
+ }
+
const firstActiveEntry = this.getFirstEntryOfActiveViewTraces();
if (firstActiveEntry) {
- return TracePosition.fromTraceEntry(firstActiveEntry);
+ currentPosition = TracePosition.fromTraceEntry(firstActiveEntry);
}
- if (this.firstEntry) {
- return TracePosition.fromTraceEntry(this.firstEntry);
+
+ if (
+ this.lastReturnedCurrentPosition === undefined ||
+ currentPosition === undefined ||
+ !this.lastReturnedCurrentPosition.isEqual(currentPosition)
+ ) {
+ this.lastReturnedCurrentPosition = currentPosition;
}
- return undefined;
+
+ return this.lastReturnedCurrentPosition;
}
setPosition(position: TracePosition | undefined) {
@@ -96,6 +104,24 @@
this.explicitlySetPosition = position;
}
+ makePositionFromActiveTrace(timestamp: Timestamp): TracePosition {
+ let trace: Trace<object> | undefined;
+ if (this.activeViewTraceTypes.length > 0) {
+ trace = this.traces.getTrace(this.activeViewTraceTypes[0]);
+ }
+
+ if (!trace) {
+ return TracePosition.fromTimestamp(timestamp);
+ }
+
+ const entry = trace.findClosestEntry(timestamp);
+ if (!entry) {
+ return TracePosition.fromTimestamp(timestamp);
+ }
+
+ return TracePosition.fromTraceEntry(entry, timestamp);
+ }
+
setActiveViewTraceTypes(types: TraceType[]) {
this.activeViewTraceTypes = types;
}
@@ -104,14 +130,26 @@
return this.timestampType;
}
+ private lastReturnedFullTimeRange?: TimeRange;
getFullTimeRange(): TimeRange {
if (!this.firstEntry || !this.lastEntry) {
throw Error('Trying to get full time range when there are no timestamps');
}
- return {
+
+ const fullTimeRange = {
from: this.firstEntry.getTimestamp(),
to: this.lastEntry.getTimestamp(),
};
+
+ if (
+ this.lastReturnedFullTimeRange === undefined ||
+ this.lastReturnedFullTimeRange.from.getValueNs() !== fullTimeRange.from.getValueNs() ||
+ this.lastReturnedFullTimeRange.to.getValueNs() !== fullTimeRange.to.getValueNs()
+ ) {
+ this.lastReturnedFullTimeRange = fullTimeRange;
+ }
+
+ return this.lastReturnedFullTimeRange;
}
getSelectionTimeRange(): TimeRange {
@@ -126,6 +164,18 @@
this.explicitlySetSelection = selection;
}
+ getZoomRange(): TimeRange {
+ if (this.explicitlySetZoomRange === undefined) {
+ return this.getFullTimeRange();
+ } else {
+ return this.explicitlySetZoomRange;
+ }
+ }
+
+ setZoom(zoomRange: TimeRange) {
+ this.explicitlySetZoomRange = zoomRange;
+ }
+
getTraces(): Traces {
return this.traces;
}
@@ -192,15 +242,23 @@
return trace.getEntry(currentIndex + 1);
}
+ private lastReturnedCurrentEntries: Map<TraceType, TraceEntry<any> | undefined> = new Map();
findCurrentEntryFor(type: TraceType): TraceEntry<{}> | undefined {
const position = this.getCurrentPosition();
if (!position) {
return undefined;
}
- return TraceEntryFinder.findCorrespondingEntry(
+
+ const entry = TraceEntryFinder.findCorrespondingEntry(
assertDefined(this.traces.getTrace(type)),
position
);
+
+ if (this.lastReturnedCurrentEntries.get(type)?.getIndex() !== entry?.getIndex()) {
+ this.lastReturnedCurrentEntries.set(type, entry);
+ }
+
+ return this.lastReturnedCurrentEntries.get(type);
}
moveToPreviousEntryFor(type: TraceType) {
diff --git a/tools/winscope/src/app/timeline_data_test.ts b/tools/winscope/src/app/timeline_data_test.ts
index 9d4d097..3fdde4a 100644
--- a/tools/winscope/src/app/timeline_data_test.ts
+++ b/tools/winscope/src/app/timeline_data_test.ts
@@ -14,16 +14,18 @@
* limitations under the License.
*/
+import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, Timestamp, TimestampType} from 'common/time';
import {TracesBuilder} from 'test/unit/traces_builder';
-import {RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {TimelineData} from './timeline_data';
describe('TimelineData', () => {
let timelineData: TimelineData;
- const timestamp10 = new Timestamp(TimestampType.REAL, 10n);
- const timestamp11 = new Timestamp(TimestampType.REAL, 11n);
+
+ const timestamp10 = new RealTimestamp(10n);
+ const timestamp11 = new RealTimestamp(11n);
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
@@ -31,11 +33,12 @@
.build();
const position10 = TracePosition.fromTraceEntry(
- traces.getTrace(TraceType.SURFACE_FLINGER)!.getEntry(0)
+ assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER)).getEntry(0)
);
const position11 = TracePosition.fromTraceEntry(
- traces.getTrace(TraceType.WINDOW_MANAGER)!.getEntry(0)
+ assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER)).getEntry(0)
);
+ const position1000 = TracePosition.fromTimestamp(new RealTimestamp(1000n));
beforeEach(() => {
timelineData = new TimelineData();
@@ -71,15 +74,14 @@
timelineData.initialize(traces, undefined);
expect(timelineData.getCurrentPosition()).toEqual(position10);
- const explicitPosition = TracePosition.fromTimestamp(new RealTimestamp(1000n));
- timelineData.setPosition(explicitPosition);
- expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+ timelineData.setPosition(position1000);
+ expect(timelineData.getCurrentPosition()).toEqual(position1000);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
- expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+ expect(timelineData.getCurrentPosition()).toEqual(position1000);
timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
- expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
+ expect(timelineData.getCurrentPosition()).toEqual(position1000);
});
it('sets active trace types and update current position accordingly', () => {
@@ -151,4 +153,72 @@
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeTrue();
}
});
+
+ it('getCurrentPosition() returns same object if no change to range', () => {
+ timelineData.initialize(traces, undefined);
+
+ expect(timelineData.getCurrentPosition()).toBe(timelineData.getCurrentPosition());
+
+ timelineData.setPosition(position11);
+
+ expect(timelineData.getCurrentPosition()).toBe(timelineData.getCurrentPosition());
+ });
+
+ it('makePositionFromActiveTrace()', () => {
+ timelineData.initialize(traces, undefined);
+ const time100 = new RealTimestamp(100n);
+
+ {
+ timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
+ const position = timelineData.makePositionFromActiveTrace(time100);
+ expect(position.timestamp).toEqual(time100);
+ expect(position.entry).toEqual(traces.getTrace(TraceType.SURFACE_FLINGER)?.getEntry(0));
+ }
+
+ {
+ timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
+ const position = timelineData.makePositionFromActiveTrace(time100);
+ expect(position.timestamp).toEqual(time100);
+ expect(position.entry).toEqual(traces.getTrace(TraceType.WINDOW_MANAGER)?.getEntry(0));
+ }
+ });
+
+ it('getFullTimeRange() returns same object if no change to range', () => {
+ timelineData.initialize(traces, undefined);
+
+ expect(timelineData.getFullTimeRange()).toBe(timelineData.getFullTimeRange());
+ });
+
+ it('getSelectionTimeRange() returns same object if no change to range', () => {
+ timelineData.initialize(traces, undefined);
+
+ expect(timelineData.getSelectionTimeRange()).toBe(timelineData.getSelectionTimeRange());
+
+ timelineData.setSelectionTimeRange({
+ from: new Timestamp(TimestampType.REAL, 0n),
+ to: new Timestamp(TimestampType.REAL, 5n),
+ });
+
+ expect(timelineData.getSelectionTimeRange()).toBe(timelineData.getSelectionTimeRange());
+ });
+
+ it('getZoomRange() returns same object if no change to range', () => {
+ timelineData.initialize(traces, undefined);
+
+ expect(timelineData.getZoomRange()).toBe(timelineData.getZoomRange());
+
+ timelineData.setZoom({
+ from: new Timestamp(TimestampType.REAL, 0n),
+ to: new Timestamp(TimestampType.REAL, 5n),
+ });
+
+ expect(timelineData.getZoomRange()).toBe(timelineData.getZoomRange());
+ });
+
+ it("getCurrentPosition() prioritizes active trace's first entry", () => {
+ timelineData.initialize(traces, undefined);
+ timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
+
+ expect(timelineData.getCurrentPosition()?.timestamp).toBe(timestamp11);
+ });
});
diff --git a/tools/winscope/src/app/trace_file_filter.ts b/tools/winscope/src/app/trace_file_filter.ts
new file mode 100644
index 0000000..d0ed9ad
--- /dev/null
+++ b/tools/winscope/src/app/trace_file_filter.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {assertDefined} from 'common/assert_utils';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
+import {TraceFile} from 'trace/trace_file';
+
+export interface FilterResult {
+ perfetto?: TraceFile;
+ legacy: TraceFile[];
+}
+
+export class TraceFileFilter {
+ private static readonly BUGREPORT_SYSTRACE_PATH =
+ 'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace';
+ private static readonly BUGREPORT_LEGACY_FILES_ALLOWLIST = [
+ 'FS/data/misc/wmtrace/',
+ 'FS/data/misc/perfetto-traces/',
+ 'proto/window_CRITICAL.proto',
+ ];
+
+ async filter(files: TraceFile[], errorListener: WinscopeErrorListener): Promise<FilterResult> {
+ const bugreportMainEntry = files.find((file) => file.file.name === 'main_entry.txt');
+ const perfettoFiles = files.filter((file) => this.isPerfettoFile(file));
+ const legacyFiles = files.filter((file) => !this.isPerfettoFile(file));
+
+ if (!(await this.isBugreport(bugreportMainEntry, files))) {
+ const perfettoFile = this.pickLargestFile(perfettoFiles, errorListener);
+ return {
+ perfetto: perfettoFile,
+ legacy: legacyFiles,
+ };
+ }
+
+ return this.filterBugreport(assertDefined(bugreportMainEntry), perfettoFiles, legacyFiles);
+ }
+
+ private async isBugreport(
+ bugreportMainEntry: TraceFile | undefined,
+ files: TraceFile[]
+ ): Promise<boolean> {
+ if (!bugreportMainEntry) {
+ return false;
+ }
+ const bugreportName = (await bugreportMainEntry.file.text()).trim();
+ return (
+ files.find(
+ (file) =>
+ file.parentArchive === bugreportMainEntry.parentArchive &&
+ file.file.name === bugreportName
+ ) !== undefined
+ );
+ }
+
+ private filterBugreport(
+ bugreportMainEntry: TraceFile,
+ perfettoFiles: TraceFile[],
+ legacyFiles: TraceFile[]
+ ): FilterResult {
+ const isFileAllowlisted = (file: TraceFile) => {
+ for (const traceDir of TraceFileFilter.BUGREPORT_LEGACY_FILES_ALLOWLIST) {
+ if (file.file.name.startsWith(traceDir)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ const fileBelongsToBugreport = (file: TraceFile) =>
+ file.parentArchive === bugreportMainEntry.parentArchive;
+
+ legacyFiles = legacyFiles.filter((file) => {
+ return isFileAllowlisted(file) || !fileBelongsToBugreport(file);
+ });
+
+ const perfettoFile = perfettoFiles.find(
+ (file) => file.file.name === TraceFileFilter.BUGREPORT_SYSTRACE_PATH
+ );
+ return {perfetto: perfettoFile, legacy: legacyFiles};
+ }
+
+ private isPerfettoFile(file: TraceFile): boolean {
+ return file.file.name.endsWith('.pftrace') || file.file.name.endsWith('.perfetto-trace');
+ }
+
+ private pickLargestFile(
+ files: TraceFile[],
+ errorListener: WinscopeErrorListener
+ ): TraceFile | undefined {
+ if (files.length === 0) {
+ return undefined;
+ }
+ return files.reduce((largestSoFar, file) => {
+ const [largest, overridden] =
+ largestSoFar.file.size > file.file.size ? [largestSoFar, file] : [file, largestSoFar];
+ errorListener.onError(
+ new WinscopeError(WinscopeErrorType.FILE_OVERRIDDEN, overridden.getDescriptor())
+ );
+ return largest;
+ });
+ }
+}
diff --git a/tools/winscope/src/app/trace_file_filter_test.ts b/tools/winscope/src/app/trace_file_filter_test.ts
new file mode 100644
index 0000000..9ca03bf
--- /dev/null
+++ b/tools/winscope/src/app/trace_file_filter_test.ts
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
+import {UnitTestUtils} from 'test/unit/utils';
+import {TraceFile} from 'trace/trace_file';
+import {TraceFileFilter} from './trace_file_filter';
+
+describe('TraceFileFilter', () => {
+ const filter = new TraceFileFilter();
+
+ // Could be any file, we just need an instance of File to be used as a fake bugreport archive
+ const bugreportArchive = new File([new ArrayBuffer(0)], 'test_bugreport.zip') as unknown as File;
+
+ let errors: WinscopeError[];
+ let errorListener: WinscopeErrorListener;
+
+ beforeEach(() => {
+ errors = [];
+ errorListener = {
+ onError(error: WinscopeError) {
+ errors.push(error);
+ },
+ };
+ });
+
+ describe('bugreport (detects it is a bugreport)', () => {
+ it('ignores non-trace dirs', async () => {
+ const pickedBugreportFiles = [
+ makeTraceFile('FS/data/misc/wmtrace/surface_flinger.bp', bugreportArchive),
+ makeTraceFile('FS/data/misc/wmtrace/transactions.bp', bugreportArchive),
+ makeTraceFile('proto/window_CRITICAL.proto', bugreportArchive),
+ ];
+
+ const ignoredBugreportFile = makeTraceFile(
+ 'FS/data/misc/ignored-dir/wm_transition_trace.bp',
+ bugreportArchive
+ );
+
+ const bugreportFiles = [
+ await makeBugreportMainEntryTraceFile(),
+ await makeBugreportCodenameTraceFile(),
+ ...pickedBugreportFiles,
+ ignoredBugreportFile,
+ ];
+
+ // Corner case:
+ // A plain trace file is loaded along the bugreport
+ // -> trace file must not be ignored
+ //
+ // Note:
+ // The even weirder corner case where two bugreports are loaded at the same time is
+ // currently not properly handled.
+ const plainTraceFile = makeTraceFile(
+ 'would-be-ignored-if-was-part-of-bugreport/input_method_clients.pb'
+ );
+
+ const result = await filter.filter([...bugreportFiles, plainTraceFile], errorListener);
+ expect(result.perfetto).toBeUndefined();
+
+ const expectedLegacy = new Set([...pickedBugreportFiles, plainTraceFile]);
+ const actualLegacy = new Set(result.legacy);
+ expect(actualLegacy).toEqual(expectedLegacy);
+ });
+
+ it('picks perfetto systrace.pftrace', async () => {
+ const perfettoSystemTrace = makeTraceFile(
+ 'FS/data/misc/perfetto-traces/bugreport/systrace.pftrace',
+ bugreportArchive
+ );
+ const bugreportFiles = [
+ await makeBugreportMainEntryTraceFile(),
+ await makeBugreportCodenameTraceFile(),
+ perfettoSystemTrace,
+ makeTraceFile('FS/data/misc/perfetto-traces/other.perfetto-trace', bugreportArchive),
+ makeTraceFile('FS/data/misc/perfetto-traces/other.pftrace', bugreportArchive),
+ ];
+ const result = await filter.filter(bugreportFiles, errorListener);
+ expect(result.perfetto).toEqual(perfettoSystemTrace);
+ expect(result.legacy).toEqual([]);
+ expect(errors).toEqual([]);
+ });
+
+ it('ignores perfetto traces other than systrace.pftrace', async () => {
+ const bugreportFiles = [
+ await makeBugreportMainEntryTraceFile(),
+ await makeBugreportCodenameTraceFile(),
+ makeTraceFile('FS/data/misc/perfetto-traces/other.perfetto-trace', bugreportArchive),
+ makeTraceFile('FS/data/misc/perfetto-traces/other.pftrace', bugreportArchive),
+ ];
+ const result = await filter.filter(bugreportFiles, errorListener);
+ expect(result.perfetto).toBeUndefined();
+ expect(result.legacy).toEqual([]);
+ expect(errors).toEqual([]);
+ });
+ });
+
+ describe('plain input (no bugreport)', () => {
+ it('picks perfetto trace with .perfetto-trace extension', async () => {
+ const perfettoTrace = makeTraceFile('file.perfetto-trace');
+ const result = await filter.filter([perfettoTrace], errorListener);
+ expect(result.perfetto).toEqual(perfettoTrace);
+ expect(result.legacy).toEqual([]);
+ expect(errors).toEqual([]);
+ });
+
+ it('picks perfetto trace with .pftrace extension', async () => {
+ const pftrace = makeTraceFile('file.pftrace');
+ const result = await filter.filter([pftrace], errorListener);
+ expect(result.perfetto).toEqual(pftrace);
+ expect(result.legacy).toEqual([]);
+ expect(errors).toEqual([]);
+ });
+
+ it('picks largest perfetto trace', async () => {
+ const small = makeTraceFile('small.perfetto-trace', undefined, 10);
+ const medium = makeTraceFile('medium.perfetto-trace', undefined, 20);
+ const large = makeTraceFile('large.perfetto-trace', undefined, 30);
+ const result = await filter.filter([small, large, medium], errorListener);
+ expect(result.perfetto).toEqual(large);
+ expect(result.legacy).toEqual([]);
+ expect(errors).toEqual([
+ new WinscopeError(WinscopeErrorType.FILE_OVERRIDDEN, small.getDescriptor()),
+ new WinscopeError(WinscopeErrorType.FILE_OVERRIDDEN, medium.getDescriptor()),
+ ]);
+ });
+ });
+
+ const makeTraceFile = (filename: string, parentArchive?: File, size?: number) => {
+ size = size ?? 0;
+ const file = new File([new ArrayBuffer(size)], filename);
+ return new TraceFile(file as unknown as File, parentArchive);
+ };
+
+ const makeBugreportMainEntryTraceFile = async () => {
+ const file = await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt');
+ return new TraceFile(file, bugreportArchive);
+ };
+
+ const makeBugreportCodenameTraceFile = async () => {
+ const file = await UnitTestUtils.getFixtureFile(
+ 'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+ 'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
+ );
+ return new TraceFile(file, bugreportArchive);
+ };
+});
diff --git a/tools/winscope/src/app/trace_icons.ts b/tools/winscope/src/app/trace_icons.ts
index d7de8fa..0ed6d9b 100644
--- a/tools/winscope/src/app/trace_icons.ts
+++ b/tools/winscope/src/app/trace_icons.ts
@@ -7,9 +7,8 @@
const WAYLAND_ICON = 'filter_none';
const PROTO_LOG_ICON = 'notes';
const SYSTEM_UI_ICON = 'filter_none';
-const LAUNCHER_ICON = 'filter_none';
+const VIEW_CAPTURE_ICON = 'filter_none';
const IME_ICON = 'keyboard';
-const ACCESSIBILITY_ICON = 'filter_none';
const TAG_ICON = 'details';
const TRACE_ERROR_ICON = 'warning';
@@ -18,7 +17,6 @@
}
export const TRACE_ICONS: IconMap = {
- [TraceType.ACCESSIBILITY]: ACCESSIBILITY_ICON,
[TraceType.WINDOW_MANAGER]: WINDOW_MANAGER_ICON,
[TraceType.SURFACE_FLINGER]: SURFACE_FLINGER_ICON,
[TraceType.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
@@ -28,7 +26,9 @@
[TraceType.WAYLAND_DUMP]: WAYLAND_ICON,
[TraceType.PROTO_LOG]: PROTO_LOG_ICON,
[TraceType.SYSTEM_UI]: SYSTEM_UI_ICON,
- [TraceType.LAUNCHER]: LAUNCHER_ICON,
+ [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: VIEW_CAPTURE_ICON,
+ [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: VIEW_CAPTURE_ICON,
+ [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: VIEW_CAPTURE_ICON,
[TraceType.INPUT_METHOD_CLIENTS]: IME_ICON,
[TraceType.INPUT_METHOD_SERVICE]: IME_ICON,
[TraceType.INPUT_METHOD_MANAGER_SERVICE]: IME_ICON,
diff --git a/tools/winscope/src/app/trace_info.ts b/tools/winscope/src/app/trace_info.ts
index 75afe69..55b8418 100644
--- a/tools/winscope/src/app/trace_info.ts
+++ b/tools/winscope/src/app/trace_info.ts
@@ -23,9 +23,8 @@
const WAYLAND_ICON = 'filter_none';
const PROTO_LOG_ICON = 'notes';
const SYSTEM_UI_ICON = 'filter_none';
-const LAUNCHER_ICON = 'filter_none';
+const VIEW_CAPTURE_ICON = 'filter_none';
const IME_ICON = 'keyboard_alt';
-const ACCESSIBILITY_ICON = 'accessibility_new';
const TAG_ICON = 'details';
const TRACE_ERROR_ICON = 'warning';
const EVENT_LOG_ICON = 'description';
@@ -42,12 +41,6 @@
}
export const TRACE_INFO: TraceInfoMap = {
- [TraceType.ACCESSIBILITY]: {
- name: 'Accessibility',
- icon: ACCESSIBILITY_ICON,
- color: '#FF63B8',
- downloadArchiveDir: 'accessibility',
- },
[TraceType.WINDOW_MANAGER]: {
name: 'Window Manager',
icon: WINDOW_MANAGER_ICON,
@@ -102,18 +95,23 @@
color: '#7A86FF',
downloadArchiveDir: 'sysui',
},
- [TraceType.LAUNCHER]: {
- name: 'Launcher',
- icon: LAUNCHER_ICON,
+ [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: {
+ name: 'View Capture - Nexuslauncher',
+ icon: VIEW_CAPTURE_ICON,
color: '#137333',
- downloadArchiveDir: 'launcher',
+ downloadArchiveDir: 'vc',
},
- // TODO: Choose ViewCapture icon, color, title name, and download archive directory
- [TraceType.VIEW_CAPTURE]: {
- name: 'View Capture',
- icon: LAUNCHER_ICON,
+ [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: {
+ name: 'View Capture - Taskbar',
+ icon: VIEW_CAPTURE_ICON,
color: '#137333',
- downloadArchiveDir: 'launcher',
+ downloadArchiveDir: 'vc',
+ },
+ [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: {
+ name: 'View Capture - Taskbar Overlay',
+ icon: VIEW_CAPTURE_ICON,
+ color: '#137333',
+ downloadArchiveDir: 'vc',
},
[TraceType.INPUT_METHOD_CLIENTS]: {
name: 'IME Clients',
diff --git a/tools/winscope/src/app/trace_pipeline.ts b/tools/winscope/src/app/trace_pipeline.ts
index 2c9d8bc..741297b 100644
--- a/tools/winscope/src/app/trace_pipeline.ts
+++ b/tools/winscope/src/app/trace_pipeline.ts
@@ -14,74 +14,92 @@
* limitations under the License.
*/
-import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
-import {ParserError, ParserFactory} from 'parsers/parser_factory';
+import {FileUtils} from 'common/file_utils';
+import {TimestampType} from 'common/time';
+import {ProgressListener} from 'messaging/progress_listener';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
+import {FileAndParsers} from 'parsers/file_and_parsers';
+import {ParserFactory} from 'parsers/parser_factory';
+import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
import {TracesParserFactory} from 'parsers/traces_parser_factory';
import {FrameMapper} from 'trace/frame_mapper';
-import {Parser} from 'trace/parser';
-import {TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
+import {FilesSource} from './files_source';
+import {LoadedParsers} from './loaded_parsers';
+import {TraceFileFilter} from './trace_file_filter';
-class TracePipeline {
- private parserFactory = new ParserFactory();
+type UnzippedArchive = TraceFile[];
+
+export class TracePipeline {
+ private loadedParsers = new LoadedParsers();
+ private traceFileFilter = new TraceFileFilter();
private tracesParserFactory = new TracesParserFactory();
- private parsers: Array<Parser<object>> = [];
- private files = new Map<TraceType, TraceFile>();
private traces = new Traces();
private commonTimestampType?: TimestampType;
+ private downloadArchiveFilename?: string;
- async loadTraceFiles(
- traceFiles: TraceFile[],
- onLoadProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
- ): Promise<ParserError[]> {
- traceFiles = await this.filterBugreportFilesIfNeeded(traceFiles);
- const [fileAndParsers, parserErrors] = await this.parserFactory.createParsers(
- traceFiles,
- onLoadProgressUpdate
- );
- for (const fileAndParser of fileAndParsers) {
- this.files.set(fileAndParser.parser.getTraceType(), fileAndParser.file);
+ async loadFiles(
+ files: File[],
+ source: FilesSource,
+ errorListener: WinscopeErrorListener,
+ progressListener: ProgressListener | undefined
+ ) {
+ this.downloadArchiveFilename = this.makeDownloadArchiveFilename(files, source);
+
+ try {
+ const unzippedArchives = await this.unzipFiles(files, progressListener, errorListener);
+
+ if (unzippedArchives.length === 0) {
+ errorListener.onError(new WinscopeError(WinscopeErrorType.NO_INPUT_FILES));
+ return;
+ }
+
+ for (const unzippedArchive of unzippedArchives) {
+ await this.loadUnzippedArchive(unzippedArchive, errorListener, progressListener);
+ }
+
+ const commonTimestampType = this.getCommonTimestampType();
+
+ this.traces = new Traces();
+
+ this.loadedParsers.getParsers().forEach((parser) => {
+ const trace = Trace.fromParser(parser, commonTimestampType);
+ this.traces.setTrace(parser.getTraceType(), trace);
+ });
+
+ const tracesParsers = await this.tracesParserFactory.createParsers(this.traces);
+
+ tracesParsers.forEach((tracesParser) => {
+ const trace = Trace.fromParser(tracesParser, commonTimestampType);
+ this.traces.setTrace(trace.type, trace);
+ });
+
+ const hasTransitionTrace = this.traces
+ .mapTrace((trace) => trace.type)
+ .some((type) => type === TraceType.TRANSITION);
+ if (hasTransitionTrace) {
+ this.traces.deleteTrace(TraceType.WM_TRANSITION);
+ this.traces.deleteTrace(TraceType.SHELL_TRANSITION);
+ }
+ } finally {
+ progressListener?.onOperationFinished();
}
-
- const newParsers = fileAndParsers.map((it) => it.parser);
- this.parsers = this.parsers.concat(newParsers);
-
- const tracesParsers = await this.tracesParserFactory.createParsers(this.parsers);
-
- const allParsers = this.parsers.concat(tracesParsers);
-
- this.traces = new Traces();
- allParsers.forEach((parser) => {
- const trace = Trace.newUninitializedTrace(parser);
- this.traces?.setTrace(parser.getTraceType(), trace);
- });
-
- const hasTransitionTrace = this.traces
- .mapTrace((trace) => trace.type)
- .some((type) => type === TraceType.TRANSITION);
- if (hasTransitionTrace) {
- this.traces.deleteTrace(TraceType.WM_TRANSITION);
- this.traces.deleteTrace(TraceType.SHELL_TRANSITION);
- }
-
- return parserErrors;
}
removeTrace(trace: Trace<object>) {
- this.parsers = this.parsers.filter((parser) => parser.getTraceType() !== trace.type);
+ this.loadedParsers.remove(trace.type);
this.traces.deleteTrace(trace.type);
}
- getLoadedFiles(): Map<TraceType, TraceFile> {
- return this.files;
+ async makeZipArchiveWithLoadedTraceFiles(): Promise<Blob> {
+ return this.loadedParsers.makeZipArchive();
}
async buildTraces() {
- const commonTimestampType = this.getCommonTimestampType();
- this.traces.forEachTrace((trace) => trace.init(commonTimestampType));
await new FrameMapper(this.traces).computeMapping();
}
@@ -89,6 +107,10 @@
return this.traces;
}
+ getDownloadArchiveFilename(): string {
+ return this.downloadArchiveFilename ?? 'winscope';
+ }
+
async getScreenRecordingVideo(): Promise<undefined | Blob> {
const screenRecording = this.getTraces().getTrace(TraceType.SCREEN_RECORDING);
if (!screenRecording || screenRecording.lengthEntries === 0) {
@@ -98,45 +120,106 @@
}
clear() {
- this.parserFactory = new ParserFactory();
- this.parsers = [];
+ this.loadedParsers.clear();
this.traces = new Traces();
this.commonTimestampType = undefined;
- this.files = new Map<TraceType, TraceFile>();
+ this.downloadArchiveFilename = undefined;
}
- private async filterBugreportFilesIfNeeded(files: TraceFile[]): Promise<TraceFile[]> {
- const bugreportMainEntry = files.find((file) => file.file.name === 'main_entry.txt');
- if (!bugreportMainEntry) {
- return files;
+ private async loadUnzippedArchive(
+ unzippedArchive: UnzippedArchive,
+ errorListener: WinscopeErrorListener,
+ progressListener: ProgressListener | undefined
+ ) {
+ const filterResult = await this.traceFileFilter.filter(unzippedArchive, errorListener);
+ if (!filterResult.perfetto && filterResult.legacy.length === 0) {
+ errorListener.onError(new WinscopeError(WinscopeErrorType.NO_INPUT_FILES));
+ return;
}
- const bugreportName = (await bugreportMainEntry.file.text()).trim();
- const isBugreport = files.find((file) => file.file.name === bugreportName) !== undefined;
- if (!isBugreport) {
- return files;
+ const legacyParsers = await new ParserFactory().createParsers(
+ filterResult.legacy,
+ progressListener,
+ errorListener
+ );
+
+ let perfettoParsers: FileAndParsers | undefined;
+
+ if (filterResult.perfetto) {
+ const parsers = await new PerfettoParserFactory().createParsers(
+ filterResult.perfetto,
+ progressListener
+ );
+ perfettoParsers = new FileAndParsers(filterResult.perfetto, parsers);
}
- const BUGREPORT_FILES_ALLOWLIST = [
- 'FS/data/misc/wmtrace/',
- 'FS/data/misc/perfetto-traces/',
- 'proto/window_CRITICAL.proto',
- ];
- const isFileAllowlisted = (file: TraceFile) => {
- for (const traceDir of BUGREPORT_FILES_ALLOWLIST) {
- if (file.file.name.startsWith(traceDir)) {
- return true;
+ this.loadedParsers.addParsers(legacyParsers, perfettoParsers, errorListener);
+ }
+
+ private makeDownloadArchiveFilename(files: File[], source: FilesSource): string {
+ // set download archive file name, used to download all traces
+ let filenameWithCurrTime: string;
+ const currTime = new Date().toISOString().slice(0, -5).replace('T', '_');
+ if (!this.downloadArchiveFilename && files.length === 1) {
+ const filenameNoDir = FileUtils.removeDirFromFileName(files[0].name);
+ const filenameNoDirOrExt = FileUtils.removeExtensionFromFilename(filenameNoDir);
+ filenameWithCurrTime = `${filenameNoDirOrExt}_${currTime}`;
+ } else {
+ filenameWithCurrTime = `${source}_${currTime}`;
+ }
+
+ const archiveFilenameNoIllegalChars = filenameWithCurrTime.replace(
+ FileUtils.ILLEGAL_FILENAME_CHARACTERS_REGEX,
+ '_'
+ );
+ if (FileUtils.DOWNLOAD_FILENAME_REGEX.test(archiveFilenameNoIllegalChars)) {
+ return archiveFilenameNoIllegalChars;
+ } else {
+ console.error(
+ "Cannot convert uploaded archive filename to acceptable format for download. Defaulting download filename to 'winscope.zip'."
+ );
+ return 'winscope';
+ }
+ }
+
+ private async unzipFiles(
+ files: File[],
+ progressListener: ProgressListener | undefined,
+ errorListener: WinscopeErrorListener
+ ): Promise<UnzippedArchive[]> {
+ const unzippedArchives: UnzippedArchive[] = [];
+ const progressMessage = 'Unzipping files...';
+
+ progressListener?.onProgressUpdate(progressMessage, 0);
+
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+
+ const onSubProgressUpdate = (subPercentage: number) => {
+ const totalPercentage = (100 * i) / files.length + subPercentage / files.length;
+ progressListener?.onProgressUpdate(progressMessage, totalPercentage);
+ };
+
+ if (FileUtils.isZipFile(file)) {
+ try {
+ const subFiles = await FileUtils.unzipFile(file, onSubProgressUpdate);
+ const subTraceFiles = subFiles.map((subFile) => {
+ return new TraceFile(subFile, file);
+ });
+ unzippedArchives.push([...subTraceFiles]);
+ onSubProgressUpdate(100);
+ } catch (e) {
+ errorListener.onError(new WinscopeError(WinscopeErrorType.CORRUPTED_ARCHIVE, file.name));
}
+ } else {
+ unzippedArchives.push([new TraceFile(file, undefined)]);
+ onSubProgressUpdate(100);
}
- return false;
- };
+ }
- const fileBelongsToBugreport = (file: TraceFile) =>
- file.parentArchive === bugreportMainEntry.parentArchive;
+ progressListener?.onProgressUpdate(progressMessage, 100);
- return files.filter((file) => {
- return isFileAllowlisted(file) || !fileBelongsToBugreport(file);
- });
+ return unzippedArchives;
}
private getCommonTimestampType(): TimestampType {
@@ -146,7 +229,8 @@
const priorityOrder = [TimestampType.REAL, TimestampType.ELAPSED];
for (const type of priorityOrder) {
- if (this.parsers.every((it) => it.getTimestamps(type) !== undefined)) {
+ const parsers = Array.from(this.loadedParsers.getParsers().values());
+ if (parsers.every((it) => it.getTimestamps(type) !== undefined)) {
this.commonTimestampType = type;
return this.commonTimestampType;
}
@@ -155,5 +239,3 @@
throw Error('Failed to find common timestamp type across all traces');
}
}
-
-export {TracePipeline};
diff --git a/tools/winscope/src/app/trace_pipeline_test.ts b/tools/winscope/src/app/trace_pipeline_test.ts
index 33f03f7..8b6979e 100644
--- a/tools/winscope/src/app/trace_pipeline_test.ts
+++ b/tools/winscope/src/app/trace_pipeline_test.ts
@@ -15,32 +15,46 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {FileUtils} from 'common/file_utils';
+import {ProgressListenerStub} from 'messaging/progress_listener_stub';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
import {TracesUtils} from 'test/unit/traces_utils';
import {UnitTestUtils} from 'test/unit/utils';
-import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
+import {FilesSource} from './files_source';
import {TracePipeline} from './trace_pipeline';
describe('TracePipeline', () => {
- let validSfTraceFile: TraceFile;
- let validWmTraceFile: TraceFile;
+ let validSfFile: File;
+ let validWmFile: File;
+ let errors: WinscopeError[];
+ let progressListener: ProgressListenerStub;
let tracePipeline: TracePipeline;
beforeEach(async () => {
- validSfTraceFile = new TraceFile(
- await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb')
+ validSfFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'
);
- validWmTraceFile = new TraceFile(
- await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb')
+ validWmFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/WindowManager.pb'
);
+
+ errors = [];
+
+ progressListener = new ProgressListenerStub();
+ spyOn(progressListener, 'onProgressUpdate');
+ spyOn(progressListener, 'onOperationFinished');
+
tracePipeline = new TracePipeline();
});
it('can load valid trace files', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
- await loadValidSfWmTraces();
+ await loadFiles([validSfFile, validWmFile], FilesSource.TEST);
+ await expectLoadResult(2, []);
+ expect(tracePipeline.getDownloadArchiveFilename()).toMatch(new RegExp(`${FilesSource.TEST}_`));
expect(tracePipeline.getTraces().getSize()).toEqual(2);
const traceEntries = await TracesUtils.extractEntries(tracePipeline.getTraces());
@@ -48,88 +62,97 @@
expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan(0);
});
+ it('can set download archive filename based on files source', async () => {
+ await loadFiles([validSfFile]);
+ await expectLoadResult(1, []);
+ expect(tracePipeline.getDownloadArchiveFilename()).toMatch(new RegExp('SurfaceFlinger_'));
+
+ tracePipeline.clear();
+
+ await loadFiles([validSfFile, validWmFile], FilesSource.COLLECTED);
+ await expectLoadResult(2, []);
+ expect(tracePipeline.getDownloadArchiveFilename()).toMatch(
+ new RegExp(`${FilesSource.COLLECTED}_`)
+ );
+ });
+
+ it('can convert illegal uploaded archive filename to legal name for download archive', async () => {
+ const fileWithIllegalName = await UnitTestUtils.getFixtureFile(
+ 'traces/SF_trace&(*_with:_illegal+_characters.pb'
+ );
+ await loadFiles([fileWithIllegalName]);
+ await expectLoadResult(1, []);
+ const downloadFilename = tracePipeline.getDownloadArchiveFilename();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test(downloadFilename)).toBeTrue();
+ });
+
it('can load a new file without dropping already-loaded traces', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
- await tracePipeline.loadTraceFiles([validSfTraceFile]);
- expect(tracePipeline.getTraces().getSize()).toEqual(1);
+ await loadFiles([validSfFile]);
+ await expectLoadResult(1, []);
- await tracePipeline.loadTraceFiles([validWmTraceFile]);
- expect(tracePipeline.getTraces().getSize()).toEqual(2);
+ await loadFiles([validWmFile]);
+ await expectLoadResult(2, []);
- await tracePipeline.loadTraceFiles([validWmTraceFile]); // ignored (duplicated)
- expect(tracePipeline.getTraces().getSize()).toEqual(2);
+ // ignored (duplicated)
+ await loadFiles([validWmFile]);
+ await expectLoadResult(2, [
+ new WinscopeError(
+ WinscopeErrorType.FILE_OVERRIDDEN,
+ 'WindowManager.pb',
+ TraceType.WINDOW_MANAGER
+ ),
+ ]);
});
it('can load bugreport and ignores non-trace dirs', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
- // Could be any file, we just need an instance of File to be used as a fake bugreport archive
- const bugreportArchive = await UnitTestUtils.getFixtureFile(
- 'bugreports/bugreport_stripped.zip'
- );
-
const bugreportFiles = [
- new TraceFile(
- await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt'),
- bugreportArchive
+ await UnitTestUtils.getFixtureFile('bugreports/main_entry.txt', 'main_entry.txt'),
+ await UnitTestUtils.getFixtureFile(
+ 'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+ 'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
),
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
- 'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt'
- ),
- bugreportArchive
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+ 'FS/data/misc/wmtrace/surface_flinger.bp'
),
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
- 'FS/data/misc/wmtrace/surface_flinger.bp'
- ),
- bugreportArchive
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/Transactions.pb',
+ 'FS/data/misc/wmtrace/transactions.bp'
),
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/Transactions.pb',
- 'FS/data/misc/wmtrace/transactions.bp'
- ),
- bugreportArchive
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/WindowManager.pb',
+ 'proto/window_CRITICAL.proto'
),
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/WindowManager.pb',
- 'proto/window_CRITICAL.proto'
- ),
- bugreportArchive
- ),
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
- 'FS/data/misc/ignored-dir/wm_transition_trace.bp'
- ),
- bugreportArchive
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
+ 'FS/data/misc/ignored-dir/wm_transition_trace.bp'
),
];
+ const bugreportArchive = new File(
+ [await FileUtils.createZipArchive(bugreportFiles)],
+ 'bugreport.zip'
+ );
+
// Corner case:
- // A plain trace file is loaded along the bugreport -> trace file must not be ignored
+ // Another file is loaded along the bugreport -> the file must not be ignored
//
// Note:
// The even weirder corner case where two bugreports are loaded at the same time is
// currently not properly handled.
- const plainTraceFile = new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
- 'would-be-ignored-if-was-part-of-bugreport/input_method_clients.pb'
- )
+ const otherFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
+ 'would-be-ignored-if-was-in-bugreport-archive/input_method_clients.pb'
);
- const mergedFiles = bugreportFiles.concat([plainTraceFile]);
- const errors = await tracePipeline.loadTraceFiles(mergedFiles);
- expect(errors.length).toEqual(0);
- await tracePipeline.buildTraces();
- const traces = tracePipeline.getTraces();
+ await loadFiles([bugreportArchive, otherFile]);
+ await expectLoadResult(4, []);
+ const traces = tracePipeline.getTraces();
expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
expect(traces.getTrace(TraceType.TRANSACTIONS)).toBeDefined();
expect(traces.getTrace(TraceType.WM_TRANSITION)).toBeUndefined(); // ignored
@@ -137,63 +160,149 @@
expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeDefined();
});
- it('is robust to invalid trace files', async () => {
- const invalidTraceFiles = [
- new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
+ it('prioritizes perfetto traces over legacy traces', async () => {
+ const files = [
+ await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/Transactions.pb'),
+ await UnitTestUtils.getFixtureFile('traces/perfetto/transactions_trace.perfetto-trace'),
];
- const errors = await tracePipeline.loadTraceFiles(invalidTraceFiles);
- await tracePipeline.buildTraces();
- expect(errors.length).toEqual(1);
- expect(tracePipeline.getTraces().getSize()).toEqual(0);
+ await loadFiles(files);
+ await expectLoadResult(1, []);
+
+ const traces = tracePipeline.getTraces();
+ expect(assertDefined(traces.getTrace(TraceType.TRANSACTIONS)).getDescriptors()).toEqual([
+ 'transactions_trace.perfetto-trace',
+ ]);
+ });
+
+ it('is robust to corrupted archive', async () => {
+ const corruptedArchive = await UnitTestUtils.getFixtureFile('corrupted_archive.zip');
+
+ await loadFiles([corruptedArchive]);
+
+ await expectLoadResult(0, [
+ new WinscopeError(WinscopeErrorType.CORRUPTED_ARCHIVE, 'corrupted_archive.zip'),
+ new WinscopeError(WinscopeErrorType.NO_INPUT_FILES),
+ ]);
+ });
+
+ it('is robust to invalid trace files', async () => {
+ const invalidFiles = [await UnitTestUtils.getFixtureFile('winscope_homepage.png')];
+
+ await loadFiles(invalidFiles);
+
+ await expectLoadResult(0, [
+ new WinscopeError(WinscopeErrorType.UNSUPPORTED_FILE_FORMAT, 'winscope_homepage.png'),
+ ]);
});
it('is robust to mixed valid and invalid trace files', async () => {
expect(tracePipeline.getTraces().getSize()).toEqual(0);
const files = [
- new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
- new TraceFile(await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb')),
+ await UnitTestUtils.getFixtureFile('winscope_homepage.png'),
+ await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb'),
];
- const errors = await tracePipeline.loadTraceFiles(files);
- await tracePipeline.buildTraces();
- expect(tracePipeline.getTraces().getSize()).toEqual(1);
- expect(errors.length).toEqual(1);
+
+ await loadFiles(files);
+
+ await expectLoadResult(1, [
+ new WinscopeError(WinscopeErrorType.UNSUPPORTED_FILE_FORMAT, 'winscope_homepage.png'),
+ ]);
});
it('is robust to trace files with no entries', async () => {
const traceFilesWithNoEntries = [
- new TraceFile(await UnitTestUtils.getFixtureFile('traces/no_entries_InputMethodClients.pb')),
+ await UnitTestUtils.getFixtureFile('traces/no_entries_InputMethodClients.pb'),
];
- const errors = await tracePipeline.loadTraceFiles(traceFilesWithNoEntries);
- await tracePipeline.buildTraces();
+ await loadFiles(traceFilesWithNoEntries);
- expect(errors.length).toEqual(0);
+ await expectLoadResult(1, []);
+ });
- expect(tracePipeline.getTraces().getSize()).toEqual(1);
+ it('is robust to multiple files of same trace type in the same archive', async () => {
+ const filesOfSameTraceType = [
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+ 'file0.pb'
+ ),
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+ 'file1.pb'
+ ),
+ ];
+
+ await loadFiles(filesOfSameTraceType);
+
+ // Expect one trace to be overridden/discarded
+ await expectLoadResult(1, [
+ new WinscopeError(WinscopeErrorType.FILE_OVERRIDDEN, 'file0.pb', TraceType.SURFACE_FLINGER),
+ ]);
+ });
+
+ it('always overrides trace of same type when new file is uploaded', async () => {
+ const sfFileOrig = [
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+ 'file_orig.pb'
+ ),
+ ];
+ const sfFileNew = [
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+ 'file_new.pb'
+ ),
+ ];
+
+ await loadFiles(sfFileOrig);
+ await expectLoadResult(1, []);
+
+ // Expect original trace to be overridden/discarded
+ await loadFiles(sfFileNew);
+ await expectLoadResult(1, [
+ new WinscopeError(
+ WinscopeErrorType.FILE_OVERRIDDEN,
+ 'file_orig.pb',
+ TraceType.SURFACE_FLINGER
+ ),
+ ]);
+ });
+
+ it('can load a single legacy trace file', async () => {
+ const file = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/Transactions.pb'
+ );
+ await loadFiles([file]);
+ await expectLoadResult(1, []);
+ });
+
+ it('can load a single perfetto trace file', async () => {
+ const file = await UnitTestUtils.getFixtureFile(
+ 'traces/perfetto/transactions_trace.perfetto-trace'
+ );
+ await loadFiles([file]);
+ await expectLoadResult(1, []);
});
it('can remove traces', async () => {
- await loadValidSfWmTraces();
- expect(tracePipeline.getTraces().getSize()).toEqual(2);
+ await loadFiles([validSfFile, validWmFile]);
+ await expectLoadResult(2, []);
const sfTrace = assertDefined(tracePipeline.getTraces().getTrace(TraceType.SURFACE_FLINGER));
const wmTrace = assertDefined(tracePipeline.getTraces().getTrace(TraceType.WINDOW_MANAGER));
tracePipeline.removeTrace(sfTrace);
- await tracePipeline.buildTraces();
- expect(tracePipeline.getTraces().getSize()).toEqual(1);
+ await expectLoadResult(1, []);
tracePipeline.removeTrace(wmTrace);
- await tracePipeline.buildTraces();
- expect(tracePipeline.getTraces().getSize()).toEqual(0);
+ await expectLoadResult(0, []);
});
it('gets loaded traces', async () => {
- await loadValidSfWmTraces();
+ await loadFiles([validSfFile, validWmFile]);
+ await expectLoadResult(2, []);
const traces = tracePipeline.getTraces();
- expect(traces.getSize()).toEqual(2);
const actualTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
const expectedTraceTypes = new Set([TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]);
@@ -203,42 +312,94 @@
expect(sfTrace.getDescriptors().length).toBeGreaterThan(0);
});
- it('builds traces', async () => {
- await loadValidSfWmTraces();
- const traces = tracePipeline.getTraces();
-
- expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
- expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeDefined();
- });
-
it('gets screenrecording data', async () => {
- const traceFiles = [
- new TraceFile(
- await UnitTestUtils.getFixtureFile(
- 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
- )
+ const files = [
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
),
];
- await tracePipeline.loadTraceFiles(traceFiles);
- await tracePipeline.buildTraces();
+ await loadFiles(files);
+ await expectLoadResult(1, []);
const video = await tracePipeline.getScreenRecordingVideo();
expect(video).toBeDefined();
- expect(video!.size).toBeGreaterThan(0);
+ expect(video?.size).toBeGreaterThan(0);
+ });
+
+ it('sets traces with correct type', async () => {
+ const validTransactionsFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/Transactions.pb',
+ 'FS/data/misc/wmtrace/transactions.bp'
+ );
+ const validWmTransitionsFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb'
+ );
+ const validShellTransitionsFile = await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb'
+ );
+ await loadFiles([
+ validSfFile,
+ validWmFile,
+ validTransactionsFile,
+ validWmTransitionsFile,
+ validShellTransitionsFile,
+ ]);
+
+ await expectLoadResult(4, []);
+ const traces = tracePipeline.getTraces();
+ const loadedTypes = traces.mapTrace((trace) => trace.type);
+ for (const loadedType of loadedTypes) {
+ expect(assertDefined(traces.getTrace(loadedType)).type).toEqual(loadedType);
+ }
+ });
+
+ it('creates zip archive with loaded trace files', async () => {
+ const files = [
+ await UnitTestUtils.getFixtureFile(
+ 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4'
+ ),
+ await UnitTestUtils.getFixtureFile('traces/perfetto/transactions_trace.perfetto-trace'),
+ ];
+ await loadFiles(files);
+ await expectLoadResult(2, []);
+
+ const archiveBlob = await tracePipeline.makeZipArchiveWithLoadedTraceFiles();
+ const actualFiles = await FileUtils.unzipFile(archiveBlob);
+ const actualFilenames = actualFiles
+ .map((file) => {
+ return file.name;
+ })
+ .sort();
+
+ const expectedFilenames = [
+ 'screen_recording_metadata_v2.mp4',
+ 'transactions_trace.perfetto-trace',
+ ];
+
+ expect(actualFilenames).toEqual(expectedFilenames);
});
it('can be cleared', async () => {
- await loadValidSfWmTraces();
- expect(tracePipeline.getTraces().getSize()).toBeGreaterThan(0);
+ await loadFiles([validSfFile, validWmFile]);
+ await expectLoadResult(2, []);
tracePipeline.clear();
expect(tracePipeline.getTraces().getSize()).toEqual(0);
});
- const loadValidSfWmTraces = async () => {
- const traceFiles = [validSfTraceFile, validWmTraceFile];
- const errors = await tracePipeline.loadTraceFiles(traceFiles);
- expect(errors.length).toEqual(0);
+ async function loadFiles(files: File[], source: FilesSource = FilesSource.TEST) {
+ const errorListener = {
+ onError(error: WinscopeError) {
+ errors.push(error);
+ },
+ };
+ await tracePipeline.loadFiles(files, source, errorListener, progressListener);
+ expect(progressListener.onOperationFinished).toHaveBeenCalled();
await tracePipeline.buildTraces();
- };
+ }
+
+ async function expectLoadResult(numberOfTraces: number, expectedErrors: WinscopeError[]) {
+ expect(errors).toEqual(expectedErrors);
+ expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces);
+ }
});
diff --git a/tools/winscope/src/common/assert_utils.ts b/tools/winscope/src/common/assert_utils.ts
index bc20385..f315429 100644
--- a/tools/winscope/src/common/assert_utils.ts
+++ b/tools/winscope/src/common/assert_utils.ts
@@ -20,3 +20,9 @@
}
return value;
}
+
+export function assertTrue(value: boolean, lazyErrorMessage: () => string) {
+ if (!value) {
+ throw new Error(lazyErrorMessage());
+ }
+}
diff --git a/tools/winscope/src/common/file_utils.ts b/tools/winscope/src/common/file_utils.ts
index ac2b688..1073d96 100644
--- a/tools/winscope/src/common/file_utils.ts
+++ b/tools/winscope/src/common/file_utils.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -19,7 +19,7 @@
export type OnFile = (file: File, parentArchive: File | undefined) => void;
class FileUtils {
- static getFileExtension(file: File) {
+ static getFileExtension(file: File): string | undefined {
const split = file.name.split('.');
if (split.length > 1) {
return split.pop();
@@ -27,7 +27,7 @@
return undefined;
}
- static removeDirFromFileName(name: string) {
+ static removeDirFromFileName(name: string): string {
if (name.includes('/')) {
const startIndex = name.lastIndexOf('/') + 1;
return name.slice(startIndex);
@@ -36,6 +36,15 @@
}
}
+ static removeExtensionFromFilename(name: string): string {
+ if (name.includes('.')) {
+ const lastIndex = name.lastIndexOf('.');
+ return name.slice(0, lastIndex);
+ } else {
+ return name;
+ }
+ }
+
static async createZipArchive(files: File[]): Promise<Blob> {
const zip = new JSZip();
for (let i = 0; i < files.length; i++) {
@@ -47,7 +56,7 @@
}
static async unzipFile(
- file: File,
+ file: Blob,
onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
): Promise<File[]> {
const unzippedFiles: File[] = [];
@@ -72,31 +81,13 @@
return unzippedFiles;
}
- static async unzipFilesIfNeeded(
- files: File[],
- onFile: OnFile,
- onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
- ) {
- for (let i = 0; i < files.length; i++) {
- const file = files[i];
-
- const onSubprogressUpdate = (subPercentage: number) => {
- const percentage = (100 * i) / files.length + subPercentage / files.length;
- onProgressUpdate(percentage);
- };
-
- if (FileUtils.isZipFile(file)) {
- const unzippedFile = await FileUtils.unzipFile(file, onSubprogressUpdate);
- unzippedFile.forEach((unzippedFile) => onFile(unzippedFile, file));
- } else {
- onFile(file, undefined);
- }
- }
- }
-
static isZipFile(file: File) {
return FileUtils.getFileExtension(file) === 'zip';
}
+
+ //allow: letters/numbers/underscores with delimeters . - # (except at start and end)
+ static readonly DOWNLOAD_FILENAME_REGEX = /^\w+?((|#|-|\.)\w+)+$/;
+ static readonly ILLEGAL_FILENAME_CHARACTERS_REGEX = /[^A-Za-z0-9-#._]/g;
}
export {FileUtils};
diff --git a/tools/winscope/src/common/file_utils_test.ts b/tools/winscope/src/common/file_utils_test.ts
new file mode 100644
index 0000000..c75bef6
--- /dev/null
+++ b/tools/winscope/src/common/file_utils_test.ts
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+import {UnitTestUtils} from 'test/unit/utils';
+import {FileUtils} from './file_utils';
+
+describe('FileUtils', () => {
+ it('extracts file extensions', () => {
+ expect(FileUtils.getFileExtension(new File([], 'winscope.zip'))).toEqual('zip');
+ expect(FileUtils.getFileExtension(new File([], 'win.scope.zip'))).toEqual('zip');
+ expect(FileUtils.getFileExtension(new File([], 'winscopezip'))).toEqual(undefined);
+ });
+
+ it('removes directory from filename', () => {
+ expect(FileUtils.removeDirFromFileName('test/winscope.zip')).toEqual('winscope.zip');
+ expect(FileUtils.removeDirFromFileName('test/test/winscope.zip')).toEqual('winscope.zip');
+ });
+
+ it('removes extension from filename', () => {
+ expect(FileUtils.removeExtensionFromFilename('winscope.zip')).toEqual('winscope');
+ expect(FileUtils.removeExtensionFromFilename('win.scope.zip')).toEqual('win.scope');
+ });
+
+ it('creates zip archive', async () => {
+ const zip = await FileUtils.createZipArchive([new File([], 'test_file.txt')]);
+ expect(zip).toBeInstanceOf(Blob);
+ });
+
+ it('unzips archive', async () => {
+ const validZipFile = await UnitTestUtils.getFixtureFile('traces/winscope.zip');
+ const unzippedFiles = await FileUtils.unzipFile(validZipFile);
+ expect(unzippedFiles.length).toBe(2);
+ });
+
+ it('has download filename regex that accepts all expected inputs', () => {
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('Winscope2')).toBeTrue();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('win_scope')).toBeTrue();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('win-scope')).toBeTrue();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('win.scope')).toBeTrue();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('win.sc.ope')).toBeTrue();
+ });
+
+ it('has download filename regex that rejects all expected inputs', () => {
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('w?n$cope')).toBeFalse();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('winscope.')).toBeFalse();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('w..scope')).toBeFalse();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('wins--pe')).toBeFalse();
+ expect(FileUtils.DOWNLOAD_FILENAME_REGEX.test('wi##cope')).toBeFalse();
+ });
+});
diff --git a/tools/winscope/src/common/function_utils.ts b/tools/winscope/src/common/function_utils.ts
index c5c2b3f..66cc6b2 100644
--- a/tools/winscope/src/common/function_utils.ts
+++ b/tools/winscope/src/common/function_utils.ts
@@ -24,4 +24,22 @@
static readonly DO_NOTHING_ASYNC = (): Promise<void> => {
return Promise.resolve();
};
+
+ static mixin<T extends object, U extends object>(a: T, b: U): T & U {
+ const ret = {};
+ Object.assign(ret, a);
+ Object.assign(ret, b);
+
+ const assignMethods = (dst: object, src: object) => {
+ for (const methodName of Object.getOwnPropertyNames(Object.getPrototypeOf(src))) {
+ const method = (src as any)[methodName];
+ (dst as any)[methodName] = method;
+ }
+ };
+
+ assignMethods(ret, a);
+ assignMethods(ret, b);
+
+ return ret as T & U;
+ }
}
diff --git a/tools/winscope/src/common/function_utils_test.ts b/tools/winscope/src/common/function_utils_test.ts
new file mode 100644
index 0000000..bd5406c
--- /dev/null
+++ b/tools/winscope/src/common/function_utils_test.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {FunctionUtils} from './function_utils';
+
+describe('FunctionUtils', () => {
+ class A {
+ a = 'a';
+ foo(): string {
+ return 'a';
+ }
+ }
+
+ class B {
+ b = 'b';
+ bar(): string {
+ return 'b';
+ }
+ }
+
+ it('mixin()', () => {
+ const a = new A();
+ const b = new B();
+
+ const mixin = FunctionUtils.mixin(a, b);
+
+ expect(mixin.a).toEqual('a');
+ expect(mixin.b).toEqual('b');
+ expect(mixin.foo()).toEqual('a');
+ expect(mixin.bar()).toEqual('b');
+ });
+});
diff --git a/tools/winscope/src/common/geometry_utils.ts b/tools/winscope/src/common/geometry_utils.ts
new file mode 100644
index 0000000..d426d18
--- /dev/null
+++ b/tools/winscope/src/common/geometry_utils.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Rect {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+}
+
+export interface TransformMatrix {
+ dsdx: number;
+ dtdx: number;
+ tx: number;
+ dsdy: number;
+ dtdy: number;
+ ty: number;
+}
+
+export const IDENTITY_MATRIX = {dsdx: 1, dtdx: 0, tx: 0, dsdy: 0, dtdy: 1, ty: 0};
+
+export class GeometryUtils {
+ static isPointInRect(point: Point, rect: Rect): boolean {
+ return (
+ rect.x <= point.x &&
+ point.x <= rect.x + rect.w &&
+ rect.y <= point.y &&
+ point.y <= rect.y + rect.h
+ );
+ }
+}
diff --git a/tools/winscope/src/common/global_config.ts b/tools/winscope/src/common/global_config.ts
index 7325b34..bf6046b 100644
--- a/tools/winscope/src/common/global_config.ts
+++ b/tools/winscope/src/common/global_config.ts
@@ -17,7 +17,7 @@
export type Schema = Omit<GlobalConfig, 'set'>;
class GlobalConfig {
- readonly MODE: 'DEV' | 'PROD' = 'PROD' as const;
+ readonly MODE: 'KARMA_TEST' | 'DEV' | 'PROD' = 'KARMA_TEST' as const;
set(config: Schema) {
Object.assign(this, config);
diff --git a/tools/winscope/src/common/object_utils.ts b/tools/winscope/src/common/object_utils.ts
new file mode 100644
index 0000000..24e913a
--- /dev/null
+++ b/tools/winscope/src/common/object_utils.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {assertDefined, assertTrue} from './assert_utils';
+
+class Key {
+ constructor(public key: string, public index?: number) {}
+
+ isArrayKey(): boolean {
+ return this.index !== undefined;
+ }
+}
+
+export class ObjectUtils {
+ static readonly ARRAY_KEY_REGEX = new RegExp('(.+)\\[(\\d+)\\]');
+
+ static getProperty(obj: object, path: string): any {
+ const keys = ObjectUtils.parseKeys(path);
+ keys.forEach((key) => {
+ if (obj === undefined) {
+ return;
+ }
+
+ if (key.isArrayKey()) {
+ if ((obj as any)[key.key] === undefined) {
+ return;
+ }
+ assertTrue(Array.isArray((obj as any)[key.key]), () => 'Expected to be array');
+ obj = (obj as any)[key.key][assertDefined(key.index)];
+ } else {
+ obj = (obj as any)[key.key];
+ }
+ });
+ return obj;
+ }
+
+ static setProperty(obj: object, path: string, value: any) {
+ const keys = ObjectUtils.parseKeys(path);
+
+ keys.slice(0, -1).forEach((key) => {
+ if (key.isArrayKey()) {
+ ObjectUtils.initializePropertyArrayIfNeeded(obj, key);
+ obj = (obj as any)[key.key][assertDefined(key.index)];
+ } else {
+ ObjectUtils.initializePropertyIfNeeded(obj, key.key);
+ obj = (obj as any)[key.key];
+ }
+ });
+
+ const lastKey = assertDefined(keys.at(-1));
+ if (lastKey.isArrayKey()) {
+ ObjectUtils.initializePropertyArrayIfNeeded(obj, lastKey);
+ (obj as any)[lastKey.key][assertDefined(lastKey.index)] = value;
+ } else {
+ (obj as any)[lastKey.key] = value;
+ }
+ }
+
+ private static parseKeys(path: string): Key[] {
+ return path.split('.').map((rawKey) => {
+ const match = ObjectUtils.ARRAY_KEY_REGEX.exec(rawKey);
+ if (match) {
+ return new Key(match[1], Number(match[2]));
+ }
+ return new Key(rawKey);
+ });
+ }
+
+ private static initializePropertyIfNeeded(obj: object, key: string) {
+ if ((obj as any)[key] === undefined) {
+ (obj as any)[key] = {};
+ }
+ assertTrue(typeof (obj as any)[key] === 'object', () => 'Expected to be object');
+ }
+
+ private static initializePropertyArrayIfNeeded(obj: object, key: Key) {
+ if ((obj as any)[key.key] === undefined) {
+ (obj as any)[key.key] = [];
+ }
+ if ((obj as any)[key.key][assertDefined(key.index)] === undefined) {
+ (obj as any)[key.key][assertDefined(key.index)] = {};
+ }
+ assertTrue(Array.isArray((obj as any)[key.key]), () => 'Expected to be array');
+ }
+}
diff --git a/tools/winscope/src/common/object_utils_test.ts b/tools/winscope/src/common/object_utils_test.ts
new file mode 100644
index 0000000..c09e842
--- /dev/null
+++ b/tools/winscope/src/common/object_utils_test.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {ObjectUtils} from './object_utils';
+
+describe('ObjectUtils', () => {
+ it('getField()', () => {
+ const obj = {
+ child0: {
+ key0: 'value0',
+ },
+ child1: [{key1: 'value1'}, 10],
+ };
+
+ expect(ObjectUtils.getProperty(obj, 'child0')).toEqual({key0: 'value0'});
+ expect(ObjectUtils.getProperty(obj, 'child0.key0')).toEqual('value0');
+ expect(ObjectUtils.getProperty(obj, 'child1')).toEqual([{key1: 'value1'}, 10]);
+ expect(ObjectUtils.getProperty(obj, 'child1[0]')).toEqual({key1: 'value1'});
+ expect(ObjectUtils.getProperty(obj, 'child1[0].key1')).toEqual('value1');
+ expect(ObjectUtils.getProperty(obj, 'child1[1]')).toEqual(10);
+ });
+
+ it('setField()', () => {
+ const obj = {};
+
+ ObjectUtils.setProperty(obj, 'child0.key0', 'value0');
+ expect(obj).toEqual({
+ child0: {
+ key0: 'value0',
+ },
+ });
+
+ ObjectUtils.setProperty(obj, 'child1[0].key1', 'value1');
+ ObjectUtils.setProperty(obj, 'child1[1]', 10);
+ expect(obj).toEqual({
+ child0: {
+ key0: 'value0',
+ },
+ child1: [{key1: 'value1'}, 10],
+ });
+ });
+});
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/common/padding.ts
similarity index 80%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/common/padding.ts
index d2b1744..1f6ef2a 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/common/padding.ts
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
-
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export interface Padding {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
}
diff --git a/tools/winscope/src/common/string_utils.ts b/tools/winscope/src/common/string_utils.ts
index 98192ca..48621c8 100644
--- a/tools/winscope/src/common/string_utils.ts
+++ b/tools/winscope/src/common/string_utils.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {assertTrue} from './assert_utils';
class StringUtils {
static parseBigIntStrippingUnit(s: string): bigint {
@@ -22,6 +23,76 @@
}
return BigInt(match[1]);
}
+
+ static convertCamelToSnakeCase(s: string): string {
+ const result: string[] = [];
+
+ let prevChar: string | undefined;
+ for (const currChar of s) {
+ const prevCharCouldBeWordEnd =
+ prevChar && (StringUtils.isDigit(prevChar) || StringUtils.isLowerCase(prevChar));
+ const currCharCouldBeWordStart = StringUtils.isUpperCase(currChar);
+ if (prevCharCouldBeWordEnd && currCharCouldBeWordStart) {
+ result.push('_');
+ result.push(currChar.toLowerCase());
+ } else {
+ result.push(currChar);
+ }
+ prevChar = currChar;
+ }
+
+ return result.join('');
+ }
+
+ static convertSnakeToCamelCase(s: string): string {
+ const tokens = s.split('_').filter((token) => token.length > 0);
+ const tokensCapitalized = tokens.map((token) => {
+ return StringUtils.capitalizeFirstCharIfAlpha(token);
+ });
+
+ const inputStartsWithUnderscore = s[0] === '_';
+ let result = inputStartsWithUnderscore ? '_' : '';
+ result += tokens[0];
+ for (const token of tokensCapitalized.slice(1)) {
+ if (!StringUtils.isAlpha(token[0])) {
+ result += '_';
+ }
+ result += token;
+ }
+
+ return result;
+ }
+
+ static isAlpha(char: string): boolean {
+ assertTrue(char.length === 1, () => 'Input must be a single character');
+ return char[0].toLowerCase() !== char[0].toUpperCase();
+ }
+
+ static isDigit(char: string): boolean {
+ assertTrue(char.length === 1, () => 'Input must be a single character');
+ return char >= '0' && char <= '9';
+ }
+
+ static isLowerCase(char: string): boolean {
+ assertTrue(char.length === 1, () => 'Input must be a single character');
+ return StringUtils.isAlpha(char) && char === char.toLowerCase();
+ }
+
+ static isUpperCase(char: string): boolean {
+ assertTrue(char.length === 1, () => 'Input must be a single character');
+ return StringUtils.isAlpha(char) && char === char.toUpperCase();
+ }
+
+ private static capitalizeFirstCharIfAlpha(word: string): string {
+ if (word.length === 0) {
+ return word;
+ }
+
+ if (!StringUtils.isAlpha(word[0])) {
+ return word;
+ }
+ return word[0].toUpperCase() + word.slice(1);
+ }
}
export {StringUtils};
diff --git a/tools/winscope/src/common/string_utils_test.ts b/tools/winscope/src/common/string_utils_test.ts
index 2536c44..e0b6a7a 100644
--- a/tools/winscope/src/common/string_utils_test.ts
+++ b/tools/winscope/src/common/string_utils_test.ts
@@ -36,4 +36,98 @@
expect(() => StringUtils.parseBigIntStrippingUnit('invalid')).toThrow();
expect(() => StringUtils.parseBigIntStrippingUnit('invalid 10 unit')).toThrow();
});
+
+ it('convertCamelToSnakeCase()', () => {
+ expect(StringUtils.convertCamelToSnakeCase('aaa')).toEqual('aaa');
+ expect(StringUtils.convertCamelToSnakeCase('Aaa')).toEqual('Aaa');
+ expect(StringUtils.convertCamelToSnakeCase('_aaa')).toEqual('_aaa');
+ expect(StringUtils.convertCamelToSnakeCase('_Aaa')).toEqual('_Aaa');
+
+ expect(StringUtils.convertCamelToSnakeCase('aaaBbb')).toEqual('aaa_bbb');
+ expect(StringUtils.convertCamelToSnakeCase('AaaBbb')).toEqual('Aaa_bbb');
+ expect(StringUtils.convertCamelToSnakeCase('aaa_bbb')).toEqual('aaa_bbb');
+ expect(StringUtils.convertCamelToSnakeCase('aaa_Bbb')).toEqual('aaa_Bbb');
+
+ expect(StringUtils.convertCamelToSnakeCase('aaaBbbCcc')).toEqual('aaa_bbb_ccc');
+ expect(StringUtils.convertCamelToSnakeCase('aaaBbb_ccc')).toEqual('aaa_bbb_ccc');
+ expect(StringUtils.convertCamelToSnakeCase('aaaBbb_Ccc')).toEqual('aaa_bbb_Ccc');
+
+ expect(StringUtils.convertCamelToSnakeCase('aaaBBBccc')).toEqual('aaa_bBBccc');
+ expect(StringUtils.convertCamelToSnakeCase('aaaBBBcccDDD')).toEqual('aaa_bBBccc_dDD');
+ expect(StringUtils.convertCamelToSnakeCase('aaaBBB_ccc')).toEqual('aaa_bBB_ccc');
+ expect(StringUtils.convertCamelToSnakeCase('aaaBbb_CCC')).toEqual('aaa_bbb_CCC');
+
+ expect(StringUtils.convertCamelToSnakeCase('_field_32')).toEqual('_field_32');
+ expect(StringUtils.convertCamelToSnakeCase('field_32')).toEqual('field_32');
+ expect(StringUtils.convertCamelToSnakeCase('field_32Bits')).toEqual('field_32_bits');
+ expect(StringUtils.convertCamelToSnakeCase('field_32BitsLsb')).toEqual('field_32_bits_lsb');
+ expect(StringUtils.convertCamelToSnakeCase('field_32bits')).toEqual('field_32bits');
+ expect(StringUtils.convertCamelToSnakeCase('field_32bitsLsb')).toEqual('field_32bits_lsb');
+
+ expect(StringUtils.convertCamelToSnakeCase('_aaaAaa.bbbBbb')).toEqual('_aaa_aaa.bbb_bbb');
+ expect(StringUtils.convertCamelToSnakeCase('aaaAaa.bbbBbb')).toEqual('aaa_aaa.bbb_bbb');
+ expect(StringUtils.convertCamelToSnakeCase('aaaAaa.field_32bitsLsb.bbbBbb')).toEqual(
+ 'aaa_aaa.field_32bits_lsb.bbb_bbb'
+ );
+ });
+
+ it('convertSnakeToCamelCase()', () => {
+ expect(StringUtils.convertSnakeToCamelCase('_aaa')).toEqual('_aaa');
+ expect(StringUtils.convertSnakeToCamelCase('aaa')).toEqual('aaa');
+
+ expect(StringUtils.convertSnakeToCamelCase('aaa_bbb')).toEqual('aaaBbb');
+ expect(StringUtils.convertSnakeToCamelCase('_aaa_bbb')).toEqual('_aaaBbb');
+
+ expect(StringUtils.convertSnakeToCamelCase('aaa_bbb_ccc')).toEqual('aaaBbbCcc');
+ expect(StringUtils.convertSnakeToCamelCase('_aaa_bbb_ccc')).toEqual('_aaaBbbCcc');
+
+ expect(StringUtils.convertSnakeToCamelCase('_field_32')).toEqual('_field_32');
+ expect(StringUtils.convertSnakeToCamelCase('field_32')).toEqual('field_32');
+ expect(StringUtils.convertSnakeToCamelCase('field_32_bits')).toEqual('field_32Bits');
+ expect(StringUtils.convertSnakeToCamelCase('field_32_bits_lsb')).toEqual('field_32BitsLsb');
+ expect(StringUtils.convertSnakeToCamelCase('field_32bits')).toEqual('field_32bits');
+ expect(StringUtils.convertSnakeToCamelCase('field_32bits_lsb')).toEqual('field_32bitsLsb');
+
+ expect(StringUtils.convertSnakeToCamelCase('_aaa_aaa.bbb_bbb')).toEqual('_aaaAaa.bbbBbb');
+ expect(StringUtils.convertSnakeToCamelCase('aaa_aaa.bbb_bbb')).toEqual('aaaAaa.bbbBbb');
+ expect(StringUtils.convertSnakeToCamelCase('aaa_aaa.field_32bits_lsb.bbb_bbb')).toEqual(
+ 'aaaAaa.field_32bitsLsb.bbbBbb'
+ );
+ });
+
+ it('isAlpha()', () => {
+ expect(StringUtils.isAlpha('a')).toBeTrue();
+ expect(StringUtils.isAlpha('A')).toBeTrue();
+ expect(StringUtils.isAlpha('_')).toBeFalse();
+ expect(StringUtils.isAlpha('0')).toBeFalse();
+ expect(StringUtils.isAlpha('9')).toBeFalse();
+ });
+
+ it('isDigit()', () => {
+ expect(StringUtils.isDigit('a')).toBeFalse();
+ expect(StringUtils.isDigit('A')).toBeFalse();
+ expect(StringUtils.isDigit('_')).toBeFalse();
+ expect(StringUtils.isDigit('0')).toBeTrue();
+ expect(StringUtils.isDigit('9')).toBeTrue();
+ });
+
+ it('isLowerCase()', () => {
+ expect(StringUtils.isLowerCase('a')).toBeTrue();
+ expect(StringUtils.isLowerCase('z')).toBeTrue();
+ expect(StringUtils.isLowerCase('A')).toBeFalse();
+ expect(StringUtils.isLowerCase('Z')).toBeFalse();
+ expect(StringUtils.isLowerCase('_')).toBeFalse();
+ expect(StringUtils.isLowerCase('0')).toBeFalse();
+ expect(StringUtils.isLowerCase('9')).toBeFalse();
+ });
+
+ it('isUpperCase()', () => {
+ expect(StringUtils.isUpperCase('A')).toBeTrue();
+ expect(StringUtils.isUpperCase('Z')).toBeTrue();
+ expect(StringUtils.isUpperCase('a')).toBeFalse();
+ expect(StringUtils.isUpperCase('z')).toBeFalse();
+ expect(StringUtils.isUpperCase('_')).toBeFalse();
+ expect(StringUtils.isUpperCase('0')).toBeFalse();
+ expect(StringUtils.isUpperCase('9')).toBeFalse();
+ });
});
diff --git a/tools/winscope/src/trace/timestamp.ts b/tools/winscope/src/common/time.ts
similarity index 64%
rename from tools/winscope/src/trace/timestamp.ts
rename to tools/winscope/src/common/time.ts
index 23999d1..6450a0d 100644
--- a/tools/winscope/src/trace/timestamp.ts
+++ b/tools/winscope/src/common/time.ts
@@ -14,6 +14,11 @@
* limitations under the License.
*/
+export interface TimeRange {
+ from: Timestamp;
+ to: Timestamp;
+}
+
export enum TimestampType {
ELAPSED = 'ELAPSED',
REAL = 'REAL',
@@ -58,9 +63,43 @@
return this.getValueNs();
}
+ in(range: TimeRange): boolean {
+ if (range.from.type !== this.type || range.to.type !== this.type) {
+ throw new Error('Mismatching timestamp types');
+ }
+
+ return (
+ range.from.getValueNs() <= this.getValueNs() && this.getValueNs() <= range.to.getValueNs()
+ );
+ }
+
add(nanoseconds: bigint): Timestamp {
return new Timestamp(this.type, this.valueNs + nanoseconds);
}
+
+ plus(timestamp: Timestamp): Timestamp {
+ this.validateTimestampArithmetic(timestamp);
+ return new Timestamp(this.type, timestamp.getValueNs() + this.getValueNs());
+ }
+
+ minus(timestamp: Timestamp): Timestamp {
+ this.validateTimestampArithmetic(timestamp);
+ return new Timestamp(this.type, this.getValueNs() - timestamp.getValueNs());
+ }
+
+ times(n: bigint): Timestamp {
+ return new Timestamp(this.type, this.getValueNs() * n);
+ }
+
+ div(n: bigint): Timestamp {
+ return new Timestamp(this.type, this.getValueNs() / n);
+ }
+
+ private validateTimestampArithmetic(timestamp: Timestamp) {
+ if (timestamp.type !== this.type) {
+ throw new Error('Attemping to do timestamp arithmetic on different timestamp types');
+ }
+ }
}
export class RealTimestamp extends Timestamp {
diff --git a/tools/winscope/src/common/time_utils.ts b/tools/winscope/src/common/time_utils.ts
index 42d6cf1..3d3ed23 100644
--- a/tools/winscope/src/common/time_utils.ts
+++ b/tools/winscope/src/common/time_utils.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
export class TimeUtils {
static compareFn(a: Timestamp, b: Timestamp): number {
@@ -161,6 +161,28 @@
static readonly HUMAN_ELAPSED_TIMESTAMP_REGEX =
/^(?=.)([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+ns)?$/;
static readonly HUMAN_REAL_TIMESTAMP_REGEX =
- /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])\.[0-9]{3}([0-9]{6})?Z?$/;
+ /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
static readonly NS_TIMESTAMP_REGEX = /^\s*[0-9]+(\s?ns)?\s*$/;
+
+ static min(ts1: Timestamp, ts2: Timestamp): Timestamp {
+ if (ts1.getType() !== ts2.getType()) {
+ throw new Error("Can't compare timestamps of different types");
+ }
+ if (ts2.getValueNs() < ts1.getValueNs()) {
+ return ts2;
+ }
+
+ return ts1;
+ }
+
+ static max(ts1: Timestamp, ts2: Timestamp): Timestamp {
+ if (ts1.getType() !== ts2.getType()) {
+ throw new Error("Can't compare timestamps of different types");
+ }
+ if (ts2.getValueNs() > ts1.getValueNs()) {
+ return ts2;
+ }
+
+ return ts1;
+ }
}
diff --git a/tools/winscope/src/common/time_utils_test.ts b/tools/winscope/src/common/time_utils_test.ts
index b3c24b3..ea5d3b5 100644
--- a/tools/winscope/src/common/time_utils_test.ts
+++ b/tools/winscope/src/common/time_utils_test.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from './time_utils';
describe('TimeUtils', () => {
@@ -191,6 +191,18 @@
expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.006000002')).toEqual(
new RealTimestamp(1668060294006000002n)
);
+ expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54')).toEqual(
+ new RealTimestamp(1668060294000000000n)
+ );
+ expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0')).toEqual(
+ new RealTimestamp(1668060294000000000n)
+ );
+ expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0100')).toEqual(
+ new RealTimestamp(1668060294010000000n)
+ );
+ expect(TimeUtils.parseHumanReal('2022-11-10T06:04:54.0175328')).toEqual(
+ new RealTimestamp(1668060294017532800n)
+ );
});
it('canReverseDateFormatting', () => {
@@ -208,6 +220,10 @@
expect(() => TimeUtils.parseHumanReal('100')).toThrow(invalidFormatError);
expect(() => TimeUtils.parseHumanReal('06h4m54s, 10 Nov 2022')).toThrow(invalidFormatError);
expect(() => TimeUtils.parseHumanReal('')).toThrow(invalidFormatError);
+ expect(() => TimeUtils.parseHumanReal('2022-11-10T06:04:54.')).toThrow(invalidFormatError);
+ expect(() => TimeUtils.parseHumanReal('2022-11-10T06:04:54.1234567890')).toThrow(
+ invalidFormatError
+ );
});
it('nano second regex accept all expected inputs', () => {
diff --git a/tools/winscope/src/common/times_test.ts b/tools/winscope/src/common/times_test.ts
new file mode 100644
index 0000000..4b803dd
--- /dev/null
+++ b/tools/winscope/src/common/times_test.ts
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from './time';
+
+describe('Timestamp', () => {
+ describe('from', () => {
+ it('throws when missing elapsed timestamp', () => {
+ expect(() => {
+ Timestamp.from(TimestampType.REAL, 100n);
+ }).toThrow();
+ });
+
+ it('can create real timestamp', () => {
+ const timestamp = Timestamp.from(TimestampType.REAL, 100n, 500n);
+ expect(timestamp.getType()).toBe(TimestampType.REAL);
+ expect(timestamp.getValueNs()).toBe(600n);
+ });
+
+ it('can create elapsed timestamp', () => {
+ let timestamp = Timestamp.from(TimestampType.ELAPSED, 100n, 500n);
+ expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+ expect(timestamp.getValueNs()).toBe(100n);
+
+ timestamp = Timestamp.from(TimestampType.ELAPSED, 100n);
+ expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+ expect(timestamp.getValueNs()).toBe(100n);
+ });
+ });
+
+ describe('arithmetic', () => {
+ it('can add', () => {
+ let timestamp = new RealTimestamp(10n).plus(new RealTimestamp(20n));
+ expect(timestamp.getType()).toBe(TimestampType.REAL);
+ expect(timestamp.getValueNs()).toBe(30n);
+
+ timestamp = new ElapsedTimestamp(10n).plus(new ElapsedTimestamp(20n));
+ expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+ expect(timestamp.getValueNs()).toBe(30n);
+ });
+
+ it('can subtract', () => {
+ let timestamp = new RealTimestamp(20n).minus(new RealTimestamp(10n));
+ expect(timestamp.getType()).toBe(TimestampType.REAL);
+ expect(timestamp.getValueNs()).toBe(10n);
+
+ timestamp = new ElapsedTimestamp(20n).minus(new ElapsedTimestamp(10n));
+ expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+ expect(timestamp.getValueNs()).toBe(10n);
+ });
+
+ it('can divide', () => {
+ let timestamp = new RealTimestamp(10n).div(2n);
+ expect(timestamp.getType()).toBe(TimestampType.REAL);
+ expect(timestamp.getValueNs()).toBe(5n);
+
+ timestamp = new ElapsedTimestamp(10n).div(2n);
+ expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
+ expect(timestamp.getValueNs()).toBe(5n);
+ });
+
+ it('fails between different timestamp types', () => {
+ const error = new Error('Attemping to do timestamp arithmetic on different timestamp types');
+ expect(() => {
+ new RealTimestamp(20n).minus(new ElapsedTimestamp(10n));
+ }).toThrow(error);
+ expect(() => {
+ new RealTimestamp(20n).plus(new ElapsedTimestamp(10n));
+ }).toThrow(error);
+ expect(() => {
+ new ElapsedTimestamp(20n).minus(new RealTimestamp(10n));
+ }).toThrow(error);
+ expect(() => {
+ new ElapsedTimestamp(20n).plus(new RealTimestamp(10n));
+ }).toThrow(error);
+ });
+ });
+});
diff --git a/tools/winscope/src/common/tree_utils.ts b/tools/winscope/src/common/tree_utils.ts
index f94dad9..5047551 100644
--- a/tools/winscope/src/common/tree_utils.ts
+++ b/tools/winscope/src/common/tree_utils.ts
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-interface TreeNode {
+interface TreeUtilsNode {
name: string;
- parent?: TreeNode;
- children?: TreeNode[];
+ parent?: TreeUtilsNode;
+ children?: TreeUtilsNode[];
}
-type FilterType = (node: TreeNode | undefined | null) => boolean;
+type FilterType = (node: TreeUtilsNode | undefined | null) => boolean;
class TreeUtils {
- static findDescendantNode(node: TreeNode, isTargetNode: FilterType): TreeNode | undefined {
+ static findDescendantNode(
+ node: TreeUtilsNode,
+ isTargetNode: FilterType
+ ): TreeUtilsNode | undefined {
if (isTargetNode(node)) {
return node;
}
@@ -42,7 +45,10 @@
return undefined;
}
- static findAncestorNode(node: TreeNode, isTargetNode: FilterType): TreeNode | undefined {
+ static findAncestorNode(
+ node: TreeUtilsNode,
+ isTargetNode: FilterType
+ ): TreeUtilsNode | undefined {
let ancestor = node.parent;
while (ancestor && !isTargetNode(ancestor)) {
@@ -53,7 +59,7 @@
}
static makeNodeFilter(filterString: string): FilterType {
- const filter = (item: TreeNode | undefined | null) => {
+ const filter = (item: TreeUtilsNode | undefined | null) => {
if (item) {
const regex = new RegExp(filterString, 'i');
return filterString.length === 0 || regex.test(`${item.name}`);
@@ -64,4 +70,4 @@
}
}
-export {TreeNode, TreeUtils, FilterType};
+export {TreeUtilsNode, TreeUtils, FilterType};
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/common/url_utils.ts
similarity index 75%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/common/url_utils.ts
index d2b1744..f085f85 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/common/url_utils.ts
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
-
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export class UrlUtils {
+ static getRootUrl(): string {
+ const fullUrl = window.location.href;
+ const posLastSlash = fullUrl.lastIndexOf('/');
+ return fullUrl.slice(0, posLastSlash + 1);
+ }
}
diff --git a/tools/winscope/src/cross_tool/cross_tool_protocol.ts b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
index 316c069..2b51a1f 100644
--- a/tools/winscope/src/cross_tool/cross_tool_protocol.ts
+++ b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
@@ -15,10 +15,15 @@
*/
import {FunctionUtils} from 'common/function_utils';
-import {OnBugreportReceived, RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
-import {OnTimestampReceived, RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
-import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
-import {RealTimestamp} from 'trace/timestamp';
+import {RealTimestamp, TimestampType} from 'common/time';
+import {
+ RemoteToolBugreportReceived,
+ RemoteToolTimestampReceived,
+ WinscopeEvent,
+ WinscopeEventType,
+} from 'messaging/winscope_event';
+import {EmitEvent, WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {Message, MessageBugReport, MessagePong, MessageTimestamp, MessageType} from './messages';
import {OriginAllowList} from './origin_allow_list';
@@ -26,12 +31,9 @@
constructor(readonly window: Window, readonly origin: string) {}
}
-export class CrossToolProtocol
- implements RemoteBugreportReceiver, RemoteTimestampReceiver, RemoteTimestampSender
-{
+export class CrossToolProtocol implements WinscopeEventEmitter, WinscopeEventListener {
private remoteTool?: RemoteTool;
- private onBugreportReceived: OnBugreportReceived = FunctionUtils.DO_NOTHING_ASYNC;
- private onTimestampReceived: OnTimestampReceived = FunctionUtils.DO_NOTHING_ASYNC;
+ private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor() {
window.addEventListener('message', async (event) => {
@@ -39,22 +41,30 @@
});
}
- setOnBugreportReceived(callback: OnBugreportReceived) {
- this.onBugreportReceived = callback;
+ setEmitEvent(callback: EmitEvent) {
+ this.emitEvent = callback;
}
- setOnTimestampReceived(callback: OnTimestampReceived) {
- this.onTimestampReceived = callback;
- }
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ if (!this.remoteTool) {
+ return;
+ }
- sendTimestamp(timestamp: RealTimestamp) {
- if (!this.remoteTool) {
- return;
- }
+ const timestamp = event.position.timestamp;
+ if (timestamp.getType() !== TimestampType.REAL) {
+ console.warn(
+ 'Cannot propagate timestamp change to remote tool.' +
+ ` Remote tool expects timestamp type ${TimestampType.REAL},` +
+ ` but Winscope wants to notify timestamp type ${timestamp.getType()}.`
+ );
+ return;
+ }
- const message = new MessageTimestamp(timestamp.getValueNs());
- this.remoteTool.window.postMessage(message, this.remoteTool.origin);
- console.log('Cross-tool protocol sent timestamp message:', message);
+ const message = new MessageTimestamp(timestamp.getValueNs());
+ this.remoteTool.window.postMessage(message, this.remoteTool.origin);
+ console.log('Cross-tool protocol sent timestamp message:', message);
+ });
}
private async onMessageReceived(event: MessageEvent) {
@@ -108,11 +118,11 @@
private async onMessageBugreportReceived(message: MessageBugReport) {
const timestamp =
message.timestampNs !== undefined ? new RealTimestamp(message.timestampNs) : undefined;
- await this.onBugreportReceived(message.file, timestamp);
+ await this.emitEvent(new RemoteToolBugreportReceived(message.file, timestamp));
}
private async onMessageTimestampReceived(message: MessageTimestamp) {
const timestamp = new RealTimestamp(message.timestampNs);
- await this.onTimestampReceived(timestamp);
+ await this.emitEvent(new RemoteToolTimestampReceived(timestamp));
}
}
diff --git a/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts b/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts
deleted file mode 100644
index 6451684..0000000
--- a/tools/winscope/src/cross_tool/cross_tool_protocol_stub.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.
- */
-
-import {FunctionUtils} from 'common/function_utils';
-import {OnBugreportReceived, RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
-import {OnTimestampReceived, RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
-import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
-import {RealTimestamp} from 'trace/timestamp';
-
-export class CrossToolProtocolStub
- implements RemoteBugreportReceiver, RemoteTimestampReceiver, RemoteTimestampSender
-{
- onBugreportReceived: OnBugreportReceived = FunctionUtils.DO_NOTHING_ASYNC;
- onTimestampReceived: OnTimestampReceived = FunctionUtils.DO_NOTHING_ASYNC;
-
- setOnBugreportReceived(callback: OnBugreportReceived) {
- this.onBugreportReceived = callback;
- }
-
- setOnTimestampReceived(callback: OnTimestampReceived) {
- this.onTimestampReceived = callback;
- }
-
- sendTimestamp(timestamp: RealTimestamp) {
- // do nothing
- }
-}
diff --git a/tools/winscope/src/cross_tool/origin_allow_list.ts b/tools/winscope/src/cross_tool/origin_allow_list.ts
index 55199c4..a2fe983 100644
--- a/tools/winscope/src/cross_tool/origin_allow_list.ts
+++ b/tools/winscope/src/cross_tool/origin_allow_list.ts
@@ -43,6 +43,8 @@
switch (mode) {
case 'DEV':
return OriginAllowList.ALLOW_LIST_DEV;
+ case 'KARMA_TEST':
+ return OriginAllowList.ALLOW_LIST_DEV;
case 'PROD':
return OriginAllowList.ALLOW_LIST_PROD;
default:
diff --git a/tools/winscope/src/trace/flickerlib/Configuration.json b/tools/winscope/src/flickerlib/Configuration.json
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/Configuration.json
rename to tools/winscope/src/flickerlib/Configuration.json
diff --git a/tools/winscope/src/trace/flickerlib/ObjectFormatter.ts b/tools/winscope/src/flickerlib/ObjectFormatter.ts
similarity index 98%
rename from tools/winscope/src/trace/flickerlib/ObjectFormatter.ts
rename to tools/winscope/src/flickerlib/ObjectFormatter.ts
index ba5c704..eb43ca1 100644
--- a/tools/winscope/src/trace/flickerlib/ObjectFormatter.ts
+++ b/tools/winscope/src/flickerlib/ObjectFormatter.ts
@@ -16,7 +16,7 @@
import {ArrayUtils} from 'common/array_utils';
import {PropertiesDump} from 'viewers/common/ui_tree_utils';
-import intDefMapping from '../../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
+import intDefMapping from '../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
import {
toActiveBuffer,
toColor,
@@ -177,6 +177,8 @@
return toColor(obj);
case `Long`:
return obj?.toString();
+ case `BigInt`:
+ return obj?.toString();
case `PointProto`:
return toPoint(obj);
case `PositionProto`:
diff --git a/tools/winscope/src/trace/flickerlib/README.md b/tools/winscope/src/flickerlib/README.md
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/README.md
rename to tools/winscope/src/flickerlib/README.md
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
new file mode 100644
index 0000000..397dde8
--- /dev/null
+++ b/tools/winscope/src/flickerlib/common.js
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+// Imports all the compiled common Flicker library classes and exports them
+// as clean es6 modules rather than having them be commonjs modules
+
+// WM
+const WindowManagerTrace =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerTrace;
+const WindowManagerState =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerState;
+const WindowManagerTraceEntryBuilder =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerTraceEntryBuilder;
+const Activity = require('flickerlib/flicker').android.tools.common.traces.wm.Activity;
+const Configuration = require('flickerlib/flicker').android.tools.common.traces.wm.Configuration;
+const ConfigurationContainer =
+ require('flickerlib/flicker').android.tools.common.traces.wm.ConfigurationContainer;
+const DisplayArea = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayArea;
+const DisplayContent = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayContent;
+const DisplayCutout = require('flickerlib/flicker').android.tools.common.traces.wm.DisplayCutout;
+const KeyguardControllerState =
+ require('flickerlib/flicker').android.tools.common.traces.wm.KeyguardControllerState;
+const RootWindowContainer =
+ require('flickerlib/flicker').android.tools.common.traces.wm.RootWindowContainer;
+const Task = require('flickerlib/flicker').android.tools.common.traces.wm.Task;
+const TaskFragment = require('flickerlib/flicker').android.tools.common.traces.wm.TaskFragment;
+const WindowConfiguration =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowConfiguration;
+const WindowContainer =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowContainer;
+const WindowLayoutParams =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowLayoutParams;
+const WindowManagerPolicy =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WindowManagerPolicy;
+const WindowState = require('flickerlib/flicker').android.tools.common.traces.wm.WindowState;
+const WindowToken = require('flickerlib/flicker').android.tools.common.traces.wm.WindowToken;
+
+// SF
+const HwcCompositionType =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.HwcCompositionType;
+const Layer = require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Layer;
+const LayerProperties =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerProperties;
+const LayerTraceEntry =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntry;
+const LayerTraceEntryBuilder =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntryBuilder;
+const LayersTrace =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.LayersTrace;
+const Transform =
+ require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Transform;
+const Display = require('flickerlib/flicker').android.tools.common.traces.surfaceflinger.Display;
+const Region = require('flickerlib/flicker').android.tools.common.datatypes.Region;
+
+// Event Log
+const EventLog = require('flickerlib/flicker').android.tools.common.traces.events.EventLog;
+const CujEvent = require('flickerlib/flicker').android.tools.common.traces.events.CujEvent;
+const CujType = require('flickerlib/flicker').android.tools.common.traces.events.CujType;
+const Event = require('flickerlib/flicker').android.tools.common.traces.events.Event;
+const FlickerEvent = require('flickerlib/flicker').android.tools.common.traces.events.FlickerEvent;
+const FocusEvent = require('flickerlib/flicker').android.tools.common.traces.events.FocusEvent;
+const EventLogParser =
+ require('flickerlib/flicker').android.tools.common.parsers.events.EventLogParser;
+const CujTrace = require('flickerlib/flicker').android.tools.common.parsers.events.CujTrace;
+const Cuj = require('flickerlib/flicker').android.tools.common.parsers.events.Cuj;
+
+// Transitions
+const Transition = require('flickerlib/flicker').android.tools.common.traces.wm.Transition;
+const TransitionType = require('flickerlib/flicker').android.tools.common.traces.wm.TransitionType;
+const TransitionChange =
+ require('flickerlib/flicker').android.tools.common.traces.wm.TransitionChange;
+const TransitionsTrace =
+ require('flickerlib/flicker').android.tools.common.traces.wm.TransitionsTrace;
+const ShellTransitionData =
+ require('flickerlib/flicker').android.tools.common.traces.wm.ShellTransitionData;
+const WmTransitionData =
+ require('flickerlib/flicker').android.tools.common.traces.wm.WmTransitionData;
+
+// Common
+const Size = require('flickerlib/flicker').android.tools.common.datatypes.Size;
+const ActiveBuffer = require('flickerlib/flicker').android.tools.common.datatypes.ActiveBuffer;
+const Color = require('flickerlib/flicker').android.tools.common.datatypes.Color;
+const Insets = require('flickerlib/flicker').android.tools.common.datatypes.Insets;
+const Matrix33 = require('flickerlib/flicker').android.tools.common.datatypes.Matrix33;
+const PlatformConsts = require('flickerlib/flicker').android.tools.common.PlatformConsts;
+const Rotation = require('flickerlib/flicker').android.tools.common.Rotation;
+const Point = require('flickerlib/flicker').android.tools.common.datatypes.Point;
+const PointF = require('flickerlib/flicker').android.tools.common.datatypes.PointF;
+const Rect = require('flickerlib/flicker').android.tools.common.datatypes.Rect;
+const RectF = require('flickerlib/flicker').android.tools.common.datatypes.RectF;
+const WindowingMode = require('flickerlib/flicker').android.tools.common.traces.wm.WindowingMode;
+const CrossPlatform = require('flickerlib/flicker').android.tools.common.CrossPlatform;
+const Timestamp = require('flickerlib/flicker').android.tools.common.Timestamp;
+const TimestampFactory = require('flickerlib/flicker').android.tools.common.TimestampFactory;
+
+const NoCache = require('flickerlib/flicker').android.tools.common.NoCache;
+
+const EMPTY_SIZE = Size.Companion.EMPTY;
+const EMPTY_BUFFER = ActiveBuffer.Companion.EMPTY;
+const EMPTY_COLOR = Color.Companion.EMPTY;
+const EMPTY_INSETS = Insets.Companion.EMPTY;
+const EMPTY_RECT = Rect.Companion.EMPTY;
+const EMPTY_RECTF = RectF.Companion.EMPTY;
+const EMPTY_POINT = Point.Companion.EMPTY;
+const EMPTY_POINTF = PointF.Companion.EMPTY;
+const EMPTY_MATRIX33 = Matrix33.Companion.identity(0, 0);
+const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
+
+function toSize(proto) {
+ if (proto == null) {
+ return EMPTY_SIZE;
+ }
+ const width = proto.width ?? proto.w ?? 0;
+ const height = proto.height ?? proto.h ?? 0;
+ if (width || height) {
+ return new Size(width, height);
+ }
+ return EMPTY_SIZE;
+}
+
+function toActiveBuffer(proto) {
+ const width = proto?.width ?? 0;
+ const height = proto?.height ?? 0;
+ const stride = proto?.stride ?? 0;
+ const format = proto?.format ?? 0;
+
+ if (width || height || stride || format) {
+ return new ActiveBuffer(width, height, stride, format);
+ }
+ return EMPTY_BUFFER;
+}
+
+function toColor(proto, hasAlpha = true) {
+ if (proto == null) {
+ return EMPTY_COLOR;
+ }
+ const r = proto.r ?? 0;
+ const g = proto.g ?? 0;
+ const b = proto.b ?? 0;
+ let a = proto.a;
+ if (a === null && !hasAlpha) {
+ a = 1;
+ }
+ if (r || g || b || a) {
+ return new Color(r, g, b, a);
+ }
+ return EMPTY_COLOR;
+}
+
+function toPoint(proto) {
+ if (proto == null) {
+ return null;
+ }
+ const x = proto.x ?? 0;
+ const y = proto.y ?? 0;
+ if (x || y) {
+ return new Point(x, y);
+ }
+ return EMPTY_POINT;
+}
+
+function toPointF(proto) {
+ if (proto == null) {
+ return null;
+ }
+ const x = proto.x ?? 0;
+ const y = proto.y ?? 0;
+ if (x || y) {
+ return new PointF(x, y);
+ }
+ return EMPTY_POINTF;
+}
+
+function toInsets(proto) {
+ if (proto == null) {
+ return EMPTY_INSETS;
+ }
+
+ const left = proto?.left ?? 0;
+ const top = proto?.top ?? 0;
+ const right = proto?.right ?? 0;
+ const bottom = proto?.bottom ?? 0;
+ if (left || top || right || bottom) {
+ return new Insets(left, top, right, bottom);
+ }
+ return EMPTY_INSETS;
+}
+
+function toCropRect(proto) {
+ if (proto == null) return EMPTY_RECT;
+
+ const right = proto.right || 0;
+ const left = proto.left || 0;
+ const bottom = proto.bottom || 0;
+ const top = proto.top || 0;
+
+ // crop (0,0) (-1,-1) means no crop
+ if (right == -1 && left == 0 && bottom == -1 && top == 0) EMPTY_RECT;
+
+ if (right - left <= 0 || bottom - top <= 0) return EMPTY_RECT;
+
+ return Rect.Companion.from(left, top, right, bottom);
+}
+
+function toRect(proto) {
+ if (proto == null) {
+ return EMPTY_RECT;
+ }
+
+ const left = proto?.left ?? 0;
+ const top = proto?.top ?? 0;
+ const right = proto?.right ?? 0;
+ const bottom = proto?.bottom ?? 0;
+ if (left || top || right || bottom) {
+ return new Rect(left, top, right, bottom);
+ }
+ return EMPTY_RECT;
+}
+
+function toRectF(proto) {
+ if (proto == null) {
+ return EMPTY_RECTF;
+ }
+
+ const left = proto?.left ?? 0;
+ const top = proto?.top ?? 0;
+ const right = proto?.right ?? 0;
+ const bottom = proto?.bottom ?? 0;
+ if (left || top || right || bottom) {
+ return new RectF(left, top, right, bottom);
+ }
+ return EMPTY_RECTF;
+}
+
+function toRegion(proto) {
+ if (proto == null) {
+ return null;
+ }
+
+ const rects = [];
+ for (let x = 0; x < proto.rect.length; x++) {
+ const rect = proto.rect[x];
+ const parsedRect = toRect(rect);
+ rects.push(parsedRect);
+ }
+
+ return new Region(rects);
+}
+
+function toTransform(proto) {
+ if (proto == null) {
+ return EMPTY_TRANSFORM;
+ }
+ const dsdx = proto.dsdx ?? 0;
+ const dtdx = proto.dtdx ?? 0;
+ const tx = proto.tx ?? 0;
+ const dsdy = proto.dsdy ?? 0;
+ const dtdy = proto.dtdy ?? 0;
+ const ty = proto.ty ?? 0;
+
+ if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
+ const matrix = new Matrix33(dsdx, dtdx, tx, dsdy, dtdy, ty);
+ return new Transform(proto.type ?? 0, matrix);
+ }
+
+ if (proto.type) {
+ return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
+ }
+ return EMPTY_TRANSFORM;
+}
+
+export {
+ Activity,
+ Configuration,
+ ConfigurationContainer,
+ DisplayArea,
+ DisplayContent,
+ KeyguardControllerState,
+ DisplayCutout,
+ RootWindowContainer,
+ Task,
+ TaskFragment,
+ WindowConfiguration,
+ WindowContainer,
+ WindowState,
+ WindowToken,
+ WindowLayoutParams,
+ WindowManagerPolicy,
+ WindowManagerTrace,
+ WindowManagerState,
+ WindowManagerTraceEntryBuilder,
+ // SF
+ HwcCompositionType,
+ Layer,
+ LayerProperties,
+ LayerTraceEntry,
+ LayerTraceEntryBuilder,
+ LayersTrace,
+ Transform,
+ Matrix33,
+ Display,
+ // Eventlog
+ EventLog,
+ CujEvent,
+ CujType,
+ Event,
+ FlickerEvent,
+ FocusEvent,
+ EventLogParser,
+ CujTrace,
+ Cuj,
+ // Transitions
+ Transition,
+ TransitionType,
+ TransitionChange,
+ TransitionsTrace,
+ ShellTransitionData,
+ WmTransitionData,
+ // Common
+ Size,
+ ActiveBuffer,
+ Color,
+ Insets,
+ PlatformConsts,
+ Point,
+ Rect,
+ RectF,
+ Region,
+ Rotation,
+ WindowingMode,
+ CrossPlatform,
+ Timestamp,
+ TimestampFactory,
+ NoCache,
+ // Service
+ toSize,
+ toActiveBuffer,
+ toColor,
+ toCropRect,
+ toInsets,
+ toPoint,
+ toPointF,
+ toRect,
+ toRectF,
+ toRegion,
+ toTransform,
+ // Constants
+ EMPTY_BUFFER,
+ EMPTY_COLOR,
+ EMPTY_RECT,
+ EMPTY_RECTF,
+ EMPTY_POINT,
+ EMPTY_POINTF,
+ EMPTY_MATRIX33,
+ EMPTY_TRANSFORM,
+};
diff --git a/tools/winscope/src/trace/flickerlib/layers/Layer.ts b/tools/winscope/src/flickerlib/layers/Layer.ts
similarity index 75%
rename from tools/winscope/src/trace/flickerlib/layers/Layer.ts
rename to tools/winscope/src/flickerlib/layers/Layer.ts
index 077d1b8..35b41d7 100644
--- a/tools/winscope/src/trace/flickerlib/layers/Layer.ts
+++ b/tools/winscope/src/flickerlib/layers/Layer.ts
@@ -22,7 +22,6 @@
toActiveBuffer,
toColor,
toCropRect,
- toRect,
toRectF,
toRegion,
} from '../common';
@@ -35,20 +34,8 @@
const bounds = toRectF(proto.bounds);
const color = toColor(proto.color);
const screenBounds = toRectF(proto.screenBounds);
- const sourceBounds = toRectF(proto.sourceBounds);
const transform = Transform.fromProto(proto.transform, proto.position);
const bufferTransform = Transform.fromProto(proto.bufferTransform, /* position */ null);
- const hwcCrop = toRectF(proto.hwcCrop);
- const hwcFrame = toRect(proto.hwcFrame);
- const requestedColor = toColor(proto.requestedColor);
- const requestedTransform = Transform.fromProto(proto.requestedTransform, proto.requestedPosition);
- const cornerRadiusCrop = toRectF(proto.cornerRadiusCrop);
- const inputTransform = Transform.fromProto(
- proto.inputWindowInfo ? proto.inputWindowInfo.transform : null
- );
- const inputRegion = toRegion(
- proto.inputWindowInfo ? proto.inputWindowInfo.touchableRegion : null
- );
const crop: Rect = toCropRect(proto.crop);
const properties = new LayerProperties(
@@ -60,25 +47,16 @@
/* isOpaque */ proto.isOpaque,
/* shadowRadius */ proto.shadowRadius,
/* cornerRadius */ proto.cornerRadius,
- /* type */ proto.type ?? ``,
screenBounds,
transform,
- sourceBounds,
/* effectiveScalingMode */ proto.effectiveScalingMode,
bufferTransform,
/* hwcCompositionType */ new HwcCompositionType(proto.hwcCompositionType),
- hwcCrop,
- hwcFrame,
/* backgroundBlurRadius */ proto.backgroundBlurRadius,
crop,
/* isRelativeOf */ proto.isRelativeOf,
/* zOrderRelativeOfId */ proto.zOrderRelativeOf,
/* stackId */ proto.layerStack,
- requestedTransform,
- requestedColor,
- cornerRadiusCrop,
- inputTransform,
- inputRegion,
excludesCompositionState
);
@@ -87,7 +65,7 @@
/* id */ proto.id,
/*parentId */ proto.parent,
/* z */ proto.z,
- /* currFrame */ proto.currFrame,
+ /* currFrameString */ `${proto.currFrame}`,
properties
);
diff --git a/tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts b/tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
similarity index 96%
rename from tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts
rename to tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
index 66c8fb0..90ee301 100644
--- a/tools/winscope/src/trace/flickerlib/layers/LayerTraceEntry.ts
+++ b/tools/winscope/src/flickerlib/layers/LayerTraceEntry.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {ElapsedTimestamp, RealTimestamp} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {ElapsedTimestamp, RealTimestamp} from 'trace/timestamp';
import {
Display,
LayerTraceEntry,
@@ -41,6 +41,7 @@
const layers = protos.map((it) => Layer.fromProto(it, excludesCompositionState));
const displays = (displayProtos || []).map((it) => newDisplay(it));
const builder = new LayerTraceEntryBuilder()
+ .setDuplicateLayerCallback(() => {})
.setElapsedTimestamp(`${elapsedTimestamp}`)
.setLayers(layers)
.setDisplays(displays)
@@ -48,6 +49,7 @@
.setHwcBlob(hwcBlob)
.setWhere(where)
.setRealToElapsedTimeOffsetNs(`${realToElapsedTimeOffsetNs ?? 0}`);
+
const entry: LayerTraceEntry = builder.build();
addAttributes(entry, protos, realToElapsedTimeOffsetNs === undefined || useElapsedTime);
diff --git a/tools/winscope/src/trace/flickerlib/layers/Transform.ts b/tools/winscope/src/flickerlib/layers/Transform.ts
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/layers/Transform.ts
rename to tools/winscope/src/flickerlib/layers/Transform.ts
diff --git a/tools/winscope/src/trace/flickerlib/mixin.ts b/tools/winscope/src/flickerlib/mixin.ts
similarity index 100%
rename from tools/winscope/src/trace/flickerlib/mixin.ts
rename to tools/winscope/src/flickerlib/mixin.ts
diff --git a/tools/winscope/src/flickerlib/windows/Activity.ts b/tools/winscope/src/flickerlib/windows/Activity.ts
new file mode 100644
index 0000000..553b571
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/Activity.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+import {Activity, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+Activity.fromProto = (windowContainer: WindowContainer, proto: any): Activity => {
+ const entry = new Activity(
+ proto.state,
+ proto.frontOfTask,
+ proto.procId,
+ proto.translucent,
+ windowContainer
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function addAttributes(entry: Activity, proto: any) {
+ entry.proto = proto;
+ entry.kind = entry.constructor.name;
+ entry.shortName = shortenName(entry.name);
+}
+
+export {Activity};
diff --git a/tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
similarity index 60%
rename from tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts
rename to tools/winscope/src/flickerlib/windows/DisplayArea.ts
index 7ed63ea..226dbde 100644
--- a/tools/winscope/src/trace/flickerlib/windows/DisplayArea.ts
+++ b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
@@ -14,31 +14,14 @@
* limitations under the License.
*/
-import {DisplayArea} from '../common';
+import {DisplayArea, WindowContainer} from '../common';
import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-DisplayArea.fromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): DisplayArea => {
- if (proto == null) {
- return null;
- } else {
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.windowContainer,
- /* protoChildren */ proto.windowContainer?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq,
- /* nameOverride */ proto.name
- );
+DisplayArea.fromProto = (windowContainer: WindowContainer, proto: any): DisplayArea => {
+ const entry = new DisplayArea(proto.isTaskDisplayArea, windowContainer);
- const entry = new DisplayArea(proto.isTaskDisplayArea, windowContainer);
-
- addAttributes(entry, proto);
- return entry;
- }
+ addAttributes(entry, proto);
+ return entry;
};
function addAttributes(entry: DisplayArea, proto: any) {
diff --git a/tools/winscope/src/flickerlib/windows/DisplayContent.ts b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
new file mode 100644
index 0000000..c959479
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+import {
+ DisplayContent,
+ DisplayCutout,
+ Rect,
+ Rotation,
+ toInsets,
+ toRect,
+ WindowContainer,
+} from '../common';
+import {shortenName} from '../mixin';
+
+DisplayContent.fromProto = (windowContainer: WindowContainer, proto: any): DisplayContent => {
+ const displayRectWidth = proto.displayInfo?.logicalWidth ?? 0;
+ const displayRectHeight = proto.displayInfo?.logicalHeight ?? 0;
+ const appRectWidth = proto.displayInfo?.appWidth ?? 0;
+ const appRectHeight = proto.displayInfo?.appHeight ?? 0;
+ const defaultBounds = proto.pinnedStackController?.defaultBounds ?? null;
+ const movementBounds = proto.pinnedStackController?.movementBounds ?? null;
+
+ const entry = new DisplayContent(
+ proto.id,
+ proto.focusedRootTaskId,
+ proto.resumedActivity?.title ?? '',
+ proto.singleTaskInstance,
+ toRect(defaultBounds),
+ toRect(movementBounds),
+ new Rect(0, 0, displayRectWidth, displayRectHeight),
+ new Rect(0, 0, appRectWidth, appRectHeight),
+ proto.dpi,
+ proto.displayInfo?.flags ?? 0,
+ toRect(proto.displayFrames?.stableBounds),
+ proto.surfaceSize,
+ proto.focusedApp,
+ proto.appTransition?.lastUsedAppTransition ?? '',
+ proto.appTransition?.appTransitionState ?? '',
+ Rotation.Companion.getByValue(proto.displayRotation?.rotation ?? 0),
+ proto.displayRotation?.lastOrientation ?? 0,
+ createDisplayCutout(proto.displayInfo?.cutout),
+ windowContainer
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function createDisplayCutout(proto: any | null): DisplayCutout | null {
+ if (proto == null) {
+ return null;
+ } else {
+ return new DisplayCutout(
+ toInsets(proto?.insets),
+ toRect(proto?.boundLeft),
+ toRect(proto?.boundTop),
+ toRect(proto?.boundRight),
+ toRect(proto?.boundBottom),
+ toInsets(proto?.waterfallInsets)
+ );
+ }
+}
+
+function addAttributes(entry: DisplayContent, proto: any) {
+ entry.proto = proto;
+ entry.kind = entry.constructor.name;
+ entry.shortName = shortenName(entry.name);
+}
+
+export {DisplayContent};
diff --git a/tools/winscope/src/flickerlib/windows/Task.ts b/tools/winscope/src/flickerlib/windows/Task.ts
new file mode 100644
index 0000000..f99a0da
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/Task.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+import {Task, toRect, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+Task.fromProto = (windowContainer: WindowContainer, proto: any): Task => {
+ const entry = new Task(
+ proto.taskFragment?.activityType ?? proto.activityType,
+ proto.fillsParent,
+ toRect(proto.bounds),
+ proto.id,
+ proto.rootTaskId,
+ proto.taskFragment?.displayId,
+ toRect(proto.lastNonFullscreenBounds),
+ proto.realActivity,
+ proto.origActivity,
+ proto.resizeMode,
+ proto.resumedActivity?.title ?? '',
+ proto.animatingBounds,
+ proto.surfaceWidth,
+ proto.surfaceHeight,
+ proto.createdByOrganizer,
+ proto.taskFragment?.minWidth ?? proto.minWidth,
+ proto.taskFragment?.minHeight ?? proto.minHeight,
+ windowContainer
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function addAttributes(entry: Task, proto: any) {
+ entry.proto = proto;
+ entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+ entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+ entry.kind = entry.constructor.name;
+ entry.shortName = shortenName(entry.name);
+}
+
+export {Task};
diff --git a/tools/winscope/src/flickerlib/windows/TaskFragment.ts b/tools/winscope/src/flickerlib/windows/TaskFragment.ts
new file mode 100644
index 0000000..42c67cf
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/TaskFragment.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021, 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.
+ */
+
+import {TaskFragment, WindowContainer} from '../common';
+import {shortenName} from '../mixin';
+
+TaskFragment.fromProto = (windowContainer: WindowContainer, proto: any): TaskFragment => {
+ const entry = new TaskFragment(
+ proto.activityType,
+ proto.displayId,
+ proto.minWidth,
+ proto.minHeight,
+ windowContainer
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function addAttributes(entry: TaskFragment, proto: any) {
+ entry.proto = proto;
+ entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+ entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+ entry.kind = entry.constructor.name;
+ entry.shortName = shortenName(entry.name);
+}
+
+export {TaskFragment};
diff --git a/tools/winscope/src/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
new file mode 100644
index 0000000..6b13058
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+import {shortenName} from '../mixin';
+
+import {
+ Configuration,
+ ConfigurationContainer,
+ toRect,
+ WindowConfiguration,
+ WindowContainer,
+} from '../common';
+
+import {Activity} from './Activity';
+import {DisplayArea} from './DisplayArea';
+import {DisplayContent} from './DisplayContent';
+import {Task} from './Task';
+import {TaskFragment} from './TaskFragment';
+import {WindowState, WindowStateUtils} from './WindowState';
+import {WindowToken} from './WindowToken';
+
+WindowContainer.fromProto = (
+ proto: any,
+ protoChildren: any[],
+ isActivityInTree: boolean,
+ nextSeq: () => number,
+ nameOverride: string | null = null,
+ identifierOverride: string | null = null,
+ tokenOverride: any = null,
+ visibleOverride: boolean | null = null
+): WindowContainer => {
+ if (proto == null) {
+ return null;
+ }
+
+ const containerOrder = nextSeq();
+ const children = protoChildren
+ .filter((it) => it != null)
+ .map((it) => WindowContainer.childrenFromProto(it, isActivityInTree, nextSeq))
+ .filter((it) => it != null);
+
+ const identifier: any = identifierOverride ?? proto.identifier;
+ const name: string = nameOverride ?? identifier?.title ?? '';
+ const token: string = tokenOverride?.toString(16) ?? identifier?.hashCode?.toString(16) ?? '';
+
+ const config = createConfigurationContainer(proto.configurationContainer);
+ const entry = new WindowContainer(
+ name,
+ token,
+ proto.orientation,
+ proto.surfaceControl?.layerId ?? 0,
+ visibleOverride ?? proto.visible,
+ config,
+ children,
+ containerOrder
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function addAttributes(entry: WindowContainer, proto: any) {
+ entry.proto = proto;
+ entry.kind = entry.constructor.name;
+ entry.shortName = shortenName(entry.name);
+}
+
+type WindowContainerChildType =
+ | DisplayContent
+ | DisplayArea
+ | Task
+ | TaskFragment
+ | Activity
+ | WindowToken
+ | WindowState
+ | WindowContainer;
+
+WindowContainer.childrenFromProto = (
+ proto: any,
+ isActivityInTree: boolean,
+ nextSeq: () => number
+): WindowContainerChildType => {
+ if (proto.displayContent !== null) {
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.displayContent.rootDisplayArea.windowContainer,
+ /* protoChildren */ proto.displayContent.rootDisplayArea.windowContainer?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq,
+ /* nameOverride */ proto.displayContent.displayInfo?.name ?? null
+ );
+
+ return DisplayContent.fromProto(windowContainer, proto.displayContent);
+ }
+
+ if (proto.displayArea !== null) {
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.displayArea.windowContainer,
+ /* protoChildren */ proto.displayArea.windowContainer?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq,
+ /* nameOverride */ proto.displayArea.name
+ );
+
+ return DisplayArea.fromProto(windowContainer, proto.displayArea);
+ }
+
+ if (proto.task !== null) {
+ const windowContainerProto =
+ proto.task.taskFragment?.windowContainer ?? proto.task.windowContainer;
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ windowContainerProto,
+ /* protoChildren */ windowContainerProto?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq
+ );
+
+ return Task.fromProto(windowContainer, proto.task);
+ }
+
+ if (proto.taskFragment !== null) {
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.taskFragment.windowContainer,
+ /* protoChildren */ proto.taskFragment.windowContainer?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq
+ );
+
+ return TaskFragment.fromProto(windowContainer, proto.taskFragment);
+ }
+
+ if (proto.activity !== null) {
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.activity.windowToken.windowContainer,
+ /* protoChildren */ proto.activity.windowToken.windowContainer?.children ?? [],
+ /* isActivityInTree */ true,
+ /* computedZ */ nextSeq,
+ /* nameOverride */ proto.activity.name,
+ /* identifierOverride */ proto.activity.identifier
+ );
+
+ return Activity.fromProto(windowContainer, proto.activity);
+ }
+
+ if (proto.windowToken !== null) {
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.windowToken.windowContainer,
+ /* protoChildren */ proto.windowToken.windowContainer?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq,
+ /* nameOverride */ proto.windowToken.hashCode.toString(16),
+ /* identifierOverride */ null,
+ /* tokenOverride */ proto.windowToken.hashCode
+ );
+
+ return WindowToken.fromProto(windowContainer, proto.windowToken);
+ }
+
+ if (proto.window !== null) {
+ const identifierName = WindowStateUtils.getIdentifier(proto.window);
+ const name = WindowStateUtils.getName(identifierName);
+
+ const windowContainer = WindowContainer.fromProto(
+ /* proto */ proto.window.windowContainer,
+ /* protoChildren */ proto.window.windowContainer?.children ?? [],
+ /* isActivityInTree */ isActivityInTree,
+ /* computedZ */ nextSeq,
+ /* nameOverride */ name,
+ /* identifierOverride */ proto.window.identifier
+ );
+
+ return WindowState.fromProto(windowContainer, proto.window, isActivityInTree);
+ }
+
+ if (proto.windowContainer !== null) {
+ return WindowContainer.fromProto(proto.windowContainer, nextSeq);
+ }
+};
+
+function createConfigurationContainer(proto: any): ConfigurationContainer {
+ const entry = ConfigurationContainer.Companion.from(
+ createConfiguration(proto?.overrideConfiguration ?? null),
+ createConfiguration(proto?.fullConfiguration ?? null),
+ createConfiguration(proto?.mergedOverrideConfiguration ?? null)
+ );
+
+ entry.obj = entry;
+ return entry;
+}
+
+function createConfiguration(proto: any): Configuration {
+ if (proto == null) {
+ return null;
+ }
+ let windowConfiguration = null;
+
+ if (proto != null && proto.windowConfiguration != null) {
+ windowConfiguration = createWindowConfiguration(proto.windowConfiguration);
+ }
+
+ return Configuration.Companion.from(
+ windowConfiguration,
+ proto?.densityDpi ?? 0,
+ proto?.orientation ?? 0,
+ proto?.screenHeightDp ?? 0,
+ proto?.screenHeightDp ?? 0,
+ proto?.smallestScreenWidthDp ?? 0,
+ proto?.screenLayout ?? 0,
+ proto?.uiMode ?? 0
+ );
+}
+
+function createWindowConfiguration(proto: any): WindowConfiguration {
+ return WindowConfiguration.Companion.from(
+ toRect(proto.appBounds),
+ toRect(proto.bounds),
+ toRect(proto.maxBounds),
+ proto.windowingMode,
+ proto.activityType
+ );
+}
+
+export {WindowContainer};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts b/tools/winscope/src/flickerlib/windows/WindowManagerState.ts
similarity index 98%
rename from tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts
rename to tools/winscope/src/flickerlib/windows/WindowManagerState.ts
index c479f21..cae4a2f 100644
--- a/tools/winscope/src/trace/flickerlib/windows/WindowManagerState.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowManagerState.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {ElapsedTimestamp, RealTimestamp} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {ElapsedTimestamp, RealTimestamp} from 'trace/timestamp';
import {
KeyguardControllerState,
RootWindowContainer,
diff --git a/tools/winscope/src/flickerlib/windows/WindowState.ts b/tools/winscope/src/flickerlib/windows/WindowState.ts
new file mode 100644
index 0000000..22c3032
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowState.ts
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020, 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.
+ */
+
+import {Size, toRect, WindowContainer, WindowLayoutParams, WindowState} from '../common';
+import {shortenName} from '../mixin';
+
+WindowState.fromProto = (
+ windowContainer: WindowContainer,
+ proto: any,
+ isActivityInTree: boolean
+): WindowState => {
+ const windowParams = createWindowLayoutParams(proto.attributes);
+ const identifierName = WindowStateUtils.getIdentifier(proto);
+ const windowType = WindowStateUtils.getWindowType(proto, identifierName);
+
+ const entry = new WindowState(
+ windowParams,
+ proto.displayId,
+ proto.stackId,
+ proto.animator?.surface?.layer ?? 0,
+ proto.animator?.surface?.shown ?? false,
+ windowType,
+ new Size(proto.requestedWidth, proto.requestedHeight),
+ toRect(proto.surfacePosition),
+ toRect(proto.windowFrames?.frame ?? null),
+ toRect(proto.windowFrames?.containingFrame ?? null),
+ toRect(proto.windowFrames?.parentFrame ?? null),
+ toRect(proto.windowFrames?.contentFrame ?? null),
+ toRect(proto.windowFrames?.contentInsets ?? null),
+ toRect(proto.surfaceInsets),
+ toRect(proto.givenContentInsets),
+ toRect(proto.animator?.lastClipRect ?? null),
+ windowContainer,
+ /* isAppWindow */ isActivityInTree
+ );
+
+ addAttributes(entry, proto);
+ return entry;
+};
+
+function createWindowLayoutParams(proto: any): WindowLayoutParams {
+ return new WindowLayoutParams(
+ /* type */ proto?.type ?? 0,
+ /* x */ proto?.x ?? 0,
+ /* y */ proto?.y ?? 0,
+ /* width */ proto?.width ?? 0,
+ /* height */ proto?.height ?? 0,
+ /* horizontalMargin */ proto?.horizontalMargin ?? 0,
+ /* verticalMargin */ proto?.verticalMargin ?? 0,
+ /* gravity */ proto?.gravity ?? 0,
+ /* softInputMode */ proto?.softInputMode ?? 0,
+ /* format */ proto?.format ?? 0,
+ /* windowAnimations */ proto?.windowAnimations ?? 0,
+ /* alpha */ proto?.alpha ?? 0,
+ /* screenBrightness */ proto?.screenBrightness ?? 0,
+ /* buttonBrightness */ proto?.buttonBrightness ?? 0,
+ /* rotationAnimation */ proto?.rotationAnimation ?? 0,
+ /* preferredRefreshRate */ proto?.preferredRefreshRate ?? 0,
+ /* preferredDisplayModeId */ proto?.preferredDisplayModeId ?? 0,
+ /* hasSystemUiListeners */ proto?.hasSystemUiListeners ?? false,
+ /* inputFeatureFlags */ proto?.inputFeatureFlags ?? 0,
+ /* userActivityTimeout */ proto?.userActivityTimeout ?? 0,
+ /* colorMode */ proto?.colorMode ?? 0,
+ /* flags */ proto?.flags ?? 0,
+ /* privateFlags */ proto?.privateFlags ?? 0,
+ /* systemUiVisibilityFlags */ proto?.systemUiVisibilityFlags ?? 0,
+ /* subtreeSystemUiVisibilityFlags */ proto?.subtreeSystemUiVisibilityFlags ?? 0,
+ /* appearance */ proto?.appearance ?? 0,
+ /* behavior */ proto?.behavior ?? 0,
+ /* fitInsetsTypes */ proto?.fitInsetsTypes ?? 0,
+ /* fitInsetsSides */ proto?.fitInsetsSides ?? 0,
+ /* fitIgnoreVisibility */ proto?.fitIgnoreVisibility ?? false
+ );
+}
+
+export class WindowStateUtils {
+ static getWindowType(proto: any, identifierName: string): number {
+ if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
+ return WindowState.WINDOW_TYPE_STARTING;
+ } else if (proto.animatingExit) {
+ return WindowState.WINDOW_TYPE_EXITING;
+ } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
+ return WindowState.WINDOW_TYPE_STARTING;
+ }
+
+ return 0;
+ }
+
+ static getName(identifierName: string): string {
+ let name = identifierName;
+
+ if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
+ name = identifierName.substring(WindowState.STARTING_WINDOW_PREFIX.length);
+ } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
+ name = identifierName.substring(WindowState.DEBUGGER_WINDOW_PREFIX.length);
+ }
+
+ return name;
+ }
+
+ static getIdentifier(proto: any): string {
+ return proto.windowContainer.identifier?.title ?? proto.identifier?.title ?? '';
+ }
+}
+
+function addAttributes(entry: WindowState, proto: any) {
+ entry.kind = entry.constructor.name;
+ entry.rect = entry.frame;
+ entry.rect.ref = entry;
+ entry.rect.label = entry.name;
+ entry.proto = proto;
+ entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
+ entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
+ entry.shortName = shortenName(entry.name);
+}
+
+export {WindowState};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowToken.ts b/tools/winscope/src/flickerlib/windows/WindowToken.ts
similarity index 62%
rename from tools/winscope/src/trace/flickerlib/windows/WindowToken.ts
rename to tools/winscope/src/flickerlib/windows/WindowToken.ts
index 5c70122..4d755aa 100644
--- a/tools/winscope/src/trace/flickerlib/windows/WindowToken.ts
+++ b/tools/winscope/src/flickerlib/windows/WindowToken.ts
@@ -14,28 +14,10 @@
* limitations under the License.
*/
-import {WindowToken} from '../common';
+import {WindowContainer, WindowToken} from '../common';
import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-WindowToken.fromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): WindowToken => {
- if (!proto) {
- return null;
- }
-
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.windowContainer,
- /* protoChildren */ proto.windowContainer?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq,
- /* nameOverride */ proto.hashCode.toString(16),
- /* identifierOverride */ null,
- /* tokenOverride */ proto.hashCode
- );
+WindowToken.fromProto = (windowContainer: WindowContainer, proto: any): WindowToken => {
const entry = new WindowToken(windowContainer);
entry.kind = entry.constructor.name;
entry.proto = proto;
diff --git a/tools/winscope/src/index.html b/tools/winscope/src/index.html
index 4a9bd1d..1cd85a5 100644
--- a/tools/winscope/src/index.html
+++ b/tools/winscope/src/index.html
@@ -21,6 +21,15 @@
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.svg">
+ <!-- Google tag (gtag.js) -->
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-95DPZ8JGEP"></script>
+ <script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+
+ gtag('config', 'G-95DPZ8JGEP');
+ </script>
</head>
<body class="mat-app-background">
<app-root></app-root>
diff --git a/tools/winscope/src/interfaces/buganizer_attachments_download_emitter.ts b/tools/winscope/src/interfaces/buganizer_attachments_download_emitter.ts
deleted file mode 100644
index f62e57b..0000000
--- a/tools/winscope/src/interfaces/buganizer_attachments_download_emitter.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-
-export type OnBuganizerAttachmentsDownloadStart = () => void;
-export type OnBuganizerAttachmentsDownloaded = (attachments: File[]) => Promise<void>;
-
-export interface BuganizerAttachmentsDownloadEmitter {
- setOnBuganizerAttachmentsDownloadStart(callback: OnBuganizerAttachmentsDownloadStart): void;
- setOnBuganizerAttachmentsDownloaded(callback: OnBuganizerAttachmentsDownloaded): void;
-}
diff --git a/tools/winscope/src/interfaces/remote_bugreport_receiver.ts b/tools/winscope/src/interfaces/remote_bugreport_receiver.ts
deleted file mode 100644
index 992eef7..0000000
--- a/tools/winscope/src/interfaces/remote_bugreport_receiver.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-
-import {RealTimestamp} from 'trace/timestamp';
-
-export type OnBugreportReceived = (bugreport: File, timestamp?: RealTimestamp) => Promise<void>;
-
-export interface RemoteBugreportReceiver {
- setOnBugreportReceived(callback: OnBugreportReceived): void;
-}
diff --git a/tools/winscope/src/interfaces/remote_timestamp_receiver.ts b/tools/winscope/src/interfaces/remote_timestamp_receiver.ts
deleted file mode 100644
index aa813e7..0000000
--- a/tools/winscope/src/interfaces/remote_timestamp_receiver.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-
-import {RealTimestamp} from 'trace/timestamp';
-
-export type OnTimestampReceived = (timestamp: RealTimestamp) => Promise<void>;
-
-export interface RemoteTimestampReceiver {
- setOnTimestampReceived(callback: OnTimestampReceived): void;
-}
diff --git a/tools/winscope/src/interfaces/remote_timestamp_sender.ts b/tools/winscope/src/interfaces/remote_timestamp_sender.ts
deleted file mode 100644
index 03ab553..0000000
--- a/tools/winscope/src/interfaces/remote_timestamp_sender.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-import {RealTimestamp} from 'trace/timestamp';
-
-export interface RemoteTimestampSender {
- sendTimestamp(timestamp: RealTimestamp): void;
-}
diff --git a/tools/winscope/src/interfaces/runnable.ts b/tools/winscope/src/interfaces/runnable.ts
deleted file mode 100644
index e14beb8..0000000
--- a/tools/winscope/src/interfaces/runnable.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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.
- */
-
-export interface Runnable {
- run(): void;
-}
diff --git a/tools/winscope/src/interfaces/trace_data_listener.ts b/tools/winscope/src/interfaces/trace_data_listener.ts
deleted file mode 100644
index 3efc14b..0000000
--- a/tools/winscope/src/interfaces/trace_data_listener.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-
-import {Viewer} from 'viewers/viewer';
-
-export interface TraceDataListener {
- onTraceDataUnloaded(): void;
- onTraceDataLoaded(viewers: Viewer[]): void;
-}
diff --git a/tools/winscope/src/interfaces/trace_position_update_listener.ts b/tools/winscope/src/interfaces/trace_position_update_listener.ts
deleted file mode 100644
index 571f3ef..0000000
--- a/tools/winscope/src/interfaces/trace_position_update_listener.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.
- */
-
-import {TracePosition} from 'trace/trace_position';
-
-export interface TracePositionUpdateListener {
- onTracePositionUpdate(position: TracePosition): void;
-}
diff --git a/tools/winscope/src/main_prod.ts b/tools/winscope/src/main_prod.ts
index 52ce2f5..3b9d00c 100644
--- a/tools/winscope/src/main_prod.ts
+++ b/tools/winscope/src/main_prod.ts
@@ -16,6 +16,11 @@
import {enableProdMode} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app_module';
+import {globalConfig} from './common/global_config';
+
+globalConfig.set({
+ MODE: 'PROD',
+});
enableProdMode();
diff --git a/tools/winscope/src/main_component_test.ts b/tools/winscope/src/main_unit_test.ts
similarity index 87%
rename from tools/winscope/src/main_component_test.ts
rename to tools/winscope/src/main_unit_test.ts
index f6c90fd..3a2b8de 100644
--- a/tools/winscope/src/main_component_test.ts
+++ b/tools/winscope/src/main_unit_test.ts
@@ -36,6 +36,6 @@
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
-// load all tests of Angular components
-const context = require.context('./', true, /_component_test\.ts$/);
+// filter matches all "*_test.ts" files that are not within the /test/e2e/ directory
+const context = require.context('./', true, /(?<!\/test\/e2e\/.*)_test.ts$/);
context.keys().forEach(context);
diff --git a/tools/winscope/src/interfaces/progress_listener.ts b/tools/winscope/src/messaging/progress_listener.ts
similarity index 100%
rename from tools/winscope/src/interfaces/progress_listener.ts
rename to tools/winscope/src/messaging/progress_listener.ts
diff --git a/tools/winscope/src/interfaces/progress_listener_stub.ts b/tools/winscope/src/messaging/progress_listener_stub.ts
similarity index 92%
rename from tools/winscope/src/interfaces/progress_listener_stub.ts
rename to tools/winscope/src/messaging/progress_listener_stub.ts
index 57c723e..c665c45 100644
--- a/tools/winscope/src/interfaces/progress_listener_stub.ts
+++ b/tools/winscope/src/messaging/progress_listener_stub.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {ProgressListener} from 'interfaces/progress_listener';
+import {ProgressListener} from 'messaging/progress_listener';
export class ProgressListenerStub implements ProgressListener {
onProgressUpdate() {
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/messaging/user_notification_listener.ts
similarity index 86%
rename from tools/winscope/src/interfaces/user_notification_listener.ts
rename to tools/winscope/src/messaging/user_notification_listener.ts
index d2b1744..76c9913 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/messaging/user_notification_listener.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
+import {WinscopeError} from 'messaging/winscope_error';
export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+ onErrors(errors: WinscopeError[]): void;
}
diff --git a/tools/winscope/src/app/components/snack_bar_opener_stub.ts b/tools/winscope/src/messaging/user_notification_listener_stub.ts
similarity index 72%
copy from tools/winscope/src/app/components/snack_bar_opener_stub.ts
copy to tools/winscope/src/messaging/user_notification_listener_stub.ts
index 8852bc3..d572a60 100644
--- a/tools/winscope/src/app/components/snack_bar_opener_stub.ts
+++ b/tools/winscope/src/messaging/user_notification_listener_stub.ts
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {ParserError} from 'parsers/parser_factory';
+import {UserNotificationListener} from './user_notification_listener';
+import {WinscopeError} from './winscope_error';
-export class SnackBarOpenerStub implements UserNotificationListener {
- onParserErrors(errors: ParserError[]) {
+export class UserNotificationListenerStub implements UserNotificationListener {
+ onErrors(errors: WinscopeError[]) {
// do nothing
}
}
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/src/messaging/winscope_error.ts
similarity index 63%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to tools/winscope/src/messaging/winscope_error.ts
index dd03545..4ffd225 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/src/messaging/winscope_error.ts
@@ -14,10 +14,19 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+export enum WinscopeErrorType {
+ CORRUPTED_ARCHIVE,
+ NO_INPUT_FILES,
+ FILE_OVERRIDDEN,
+ UNSUPPORTED_FILE_FORMAT,
+}
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
+export class WinscopeError {
+ constructor(
+ public type: WinscopeErrorType,
+ public trace: string | undefined = undefined,
+ public traceType: TraceType | undefined = undefined
+ ) {}
}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/messaging/winscope_error_listener.ts
similarity index 80%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/messaging/winscope_error_listener.ts
index d2b1744..b69ed9a 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/messaging/winscope_error_listener.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
+import {WinscopeError} from './winscope_error';
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export interface WinscopeErrorListener {
+ onError(error: WinscopeError): void;
}
diff --git a/tools/winscope/src/app/components/snack_bar_opener_stub.ts b/tools/winscope/src/messaging/winscope_error_listener_stub.ts
similarity index 72%
copy from tools/winscope/src/app/components/snack_bar_opener_stub.ts
copy to tools/winscope/src/messaging/winscope_error_listener_stub.ts
index 8852bc3..6d5bdf2 100644
--- a/tools/winscope/src/app/components/snack_bar_opener_stub.ts
+++ b/tools/winscope/src/messaging/winscope_error_listener_stub.ts
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {ParserError} from 'parsers/parser_factory';
+import {WinscopeError} from './winscope_error';
+import {WinscopeErrorListener} from './winscope_error_listener';
-export class SnackBarOpenerStub implements UserNotificationListener {
- onParserErrors(errors: ParserError[]) {
+export class WinscopeErrorListenerStub implements WinscopeErrorListener {
+ onError(error: WinscopeError) {
// do nothing
}
}
diff --git a/tools/winscope/src/messaging/winscope_event.ts b/tools/winscope/src/messaging/winscope_event.ts
new file mode 100644
index 0000000..2963e72
--- /dev/null
+++ b/tools/winscope/src/messaging/winscope_event.ts
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {Timestamp} from 'common/time';
+import {TraceEntry} from 'trace/trace';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {View, Viewer} from 'viewers/viewer';
+
+export enum WinscopeEventType {
+ APP_INITIALIZED,
+ APP_FILES_COLLECTED,
+ APP_FILES_UPLOADED,
+ APP_RESET_REQUEST,
+ APP_TRACE_VIEW_REQUEST,
+ BUGANIZER_ATTACHMENTS_DOWNLOAD_START,
+ BUGANIZER_ATTACHMENTS_DOWNLOADED,
+ REMOTE_TOOL_BUGREPORT_RECEIVED,
+ REMOTE_TOOL_TIMESTAMP_RECEIVED,
+ TABBED_VIEW_SWITCHED,
+ TABBED_VIEW_SWITCH_REQUEST,
+ TRACE_POSITION_UPDATE,
+ VIEWERS_LOADED,
+ VIEWERS_UNLOADED,
+}
+
+interface TypeMap {
+ [WinscopeEventType.APP_INITIALIZED]: AppInitialized;
+ [WinscopeEventType.APP_FILES_COLLECTED]: AppFilesCollected;
+ [WinscopeEventType.APP_FILES_UPLOADED]: AppFilesUploaded;
+ [WinscopeEventType.APP_RESET_REQUEST]: AppResetRequest;
+ [WinscopeEventType.APP_TRACE_VIEW_REQUEST]: AppTraceViewRequest;
+ [WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOAD_START]: BuganizerAttachmentsDownloadStart;
+ [WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOADED]: BuganizerAttachmentsDownloaded;
+ [WinscopeEventType.REMOTE_TOOL_BUGREPORT_RECEIVED]: RemoteToolBugreportReceived;
+ [WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED]: RemoteToolTimestampReceived;
+ [WinscopeEventType.TABBED_VIEW_SWITCHED]: TabbedViewSwitched;
+ [WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST]: TabbedViewSwitchRequest;
+ [WinscopeEventType.TRACE_POSITION_UPDATE]: TracePositionUpdate;
+ [WinscopeEventType.VIEWERS_LOADED]: ViewersLoaded;
+ [WinscopeEventType.VIEWERS_UNLOADED]: ViewersUnloaded;
+}
+
+export abstract class WinscopeEvent {
+ abstract readonly type: WinscopeEventType;
+
+ async visit<T extends WinscopeEventType>(
+ type: T,
+ callback: (event: TypeMap[T]) => Promise<void>
+ ) {
+ if (this.type === type) {
+ const event = this as unknown as TypeMap[T];
+ await callback(event);
+ }
+ }
+}
+
+export class AppInitialized extends WinscopeEvent {
+ override readonly type = WinscopeEventType.APP_INITIALIZED;
+}
+
+export class AppFilesCollected extends WinscopeEvent {
+ override readonly type = WinscopeEventType.APP_FILES_COLLECTED;
+
+ constructor(readonly files: File[]) {
+ super();
+ }
+}
+
+export class AppFilesUploaded extends WinscopeEvent {
+ override readonly type = WinscopeEventType.APP_FILES_UPLOADED;
+
+ constructor(readonly files: File[]) {
+ super();
+ }
+}
+
+export class AppResetRequest extends WinscopeEvent {
+ override readonly type = WinscopeEventType.APP_RESET_REQUEST;
+}
+
+export class AppTraceViewRequest extends WinscopeEvent {
+ override readonly type = WinscopeEventType.APP_TRACE_VIEW_REQUEST;
+}
+
+export class BuganizerAttachmentsDownloadStart extends WinscopeEvent {
+ override readonly type = WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOAD_START;
+}
+
+export class BuganizerAttachmentsDownloaded extends WinscopeEvent {
+ override readonly type = WinscopeEventType.BUGANIZER_ATTACHMENTS_DOWNLOADED;
+
+ constructor(readonly files: File[]) {
+ super();
+ }
+}
+
+export class RemoteToolBugreportReceived extends WinscopeEvent {
+ override readonly type = WinscopeEventType.REMOTE_TOOL_BUGREPORT_RECEIVED;
+
+ constructor(readonly bugreport: File, readonly timestamp?: Timestamp) {
+ super();
+ }
+}
+
+export class RemoteToolTimestampReceived extends WinscopeEvent {
+ override readonly type = WinscopeEventType.REMOTE_TOOL_TIMESTAMP_RECEIVED;
+
+ constructor(readonly timestamp: Timestamp) {
+ super();
+ }
+}
+
+export class TabbedViewSwitched extends WinscopeEvent {
+ override readonly type = WinscopeEventType.TABBED_VIEW_SWITCHED;
+ readonly newFocusedView: View;
+
+ constructor(view: View) {
+ super();
+ this.newFocusedView = view;
+ }
+}
+
+export class TabbedViewSwitchRequest extends WinscopeEvent {
+ override readonly type = WinscopeEventType.TABBED_VIEW_SWITCH_REQUEST;
+
+ //TODO(b/263779536): use proper view/viewer ID, instead of abusing trace type.
+ readonly newFocusedViewId: TraceType;
+
+ constructor(newFocusedViewId: TraceType) {
+ super();
+ this.newFocusedViewId = newFocusedViewId;
+ }
+}
+
+export class TracePositionUpdate extends WinscopeEvent {
+ override readonly type = WinscopeEventType.TRACE_POSITION_UPDATE;
+ readonly position: TracePosition;
+
+ constructor(position: TracePosition) {
+ super();
+ this.position = position;
+ }
+
+ static fromTimestamp(timestamp: Timestamp): TracePositionUpdate {
+ const position = TracePosition.fromTimestamp(timestamp);
+ return new TracePositionUpdate(position);
+ }
+
+ static fromTraceEntry(entry: TraceEntry<object>): TracePositionUpdate {
+ const position = TracePosition.fromTraceEntry(entry);
+ return new TracePositionUpdate(position);
+ }
+}
+
+export class ViewersLoaded extends WinscopeEvent {
+ override readonly type = WinscopeEventType.VIEWERS_LOADED;
+
+ constructor(readonly viewers: Viewer[]) {
+ super();
+ }
+}
+
+export class ViewersUnloaded extends WinscopeEvent {
+ override readonly type = WinscopeEventType.VIEWERS_UNLOADED;
+}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/messaging/winscope_event_emitter.ts
similarity index 75%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/messaging/winscope_event_emitter.ts
index d2b1744..f451acd 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/messaging/winscope_event_emitter.ts
@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {WinscopeEvent} from './winscope_event';
-import {ParserError} from 'parsers/parser_factory';
+export type EmitEvent = (event: WinscopeEvent) => Promise<void>;
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export interface WinscopeEventEmitter {
+ setEmitEvent(callback: EmitEvent): void;
}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/messaging/winscope_event_emitter_stub.ts
similarity index 74%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/messaging/winscope_event_emitter_stub.ts
index d2b1744..0687bcf 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/messaging/winscope_event_emitter_stub.ts
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
+import {EmitEvent, WinscopeEventEmitter} from './winscope_event_emitter';
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export class WinscopeEventEmitterStub implements WinscopeEventEmitter {
+ setEmitEvent(callback: EmitEvent) {
+ // do nothing
+ }
}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/messaging/winscope_event_listener.ts
similarity index 80%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/messaging/winscope_event_listener.ts
index d2b1744..f85a600 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/messaging/winscope_event_listener.ts
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {WinscopeEvent} from './winscope_event';
-import {ParserError} from 'parsers/parser_factory';
-
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export interface WinscopeEventListener {
+ onWinscopeEvent(event: WinscopeEvent): Promise<void>;
}
diff --git a/tools/winscope/src/app/components/snack_bar_opener_stub.ts b/tools/winscope/src/messaging/winscope_event_listener_stub.ts
similarity index 72%
rename from tools/winscope/src/app/components/snack_bar_opener_stub.ts
rename to tools/winscope/src/messaging/winscope_event_listener_stub.ts
index 8852bc3..4b9e23c 100644
--- a/tools/winscope/src/app/components/snack_bar_opener_stub.ts
+++ b/tools/winscope/src/messaging/winscope_event_listener_stub.ts
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-import {UserNotificationListener} from 'interfaces/user_notification_listener';
-import {ParserError} from 'parsers/parser_factory';
+import {WinscopeEvent} from './winscope_event';
+import {WinscopeEventListener} from './winscope_event_listener';
-export class SnackBarOpenerStub implements UserNotificationListener {
- onParserErrors(errors: ParserError[]) {
+export class WinscopeEventListenerStub implements WinscopeEventListener {
+ async onWinscopeEvent(event: WinscopeEvent) {
// do nothing
}
}
diff --git a/tools/winscope/src/parsers/abstract_parser.ts b/tools/winscope/src/parsers/abstract_parser.ts
index ec57f46..d2b695b 100644
--- a/tools/winscope/src/parsers/abstract_parser.ts
+++ b/tools/winscope/src/parsers/abstract_parser.ts
@@ -14,37 +14,38 @@
* limitations under the License.
*/
-import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {
+ CustomQueryParamTypeMap,
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+} from 'trace/custom_query';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
+import {ParsingUtils} from './parsing_utils';
abstract class AbstractParser<T extends object = object> implements Parser<T> {
protected traceFile: TraceFile;
protected decodedEntries: any[] = [];
private timestamps: Map<TimestampType, Timestamp[]> = new Map<TimestampType, Timestamp[]>();
- protected constructor(trace: TraceFile) {
+ constructor(trace: TraceFile) {
this.traceFile = trace;
}
async parse() {
const traceBuffer = new Uint8Array(await this.traceFile.file.arrayBuffer());
+ ParsingUtils.throwIfMagicNumberDoesntMatch(traceBuffer, this.getMagicNumber());
+ this.decodedEntries = this.decodeTrace(traceBuffer).map((it) =>
+ ParsingUtils.addDefaultProtoFields(it)
+ );
+ this.timestamps = this.decodeTimestamps();
+ }
- const magicNumber = this.getMagicNumber();
- if (magicNumber !== undefined) {
- const bufferContainsMagicNumber = ArrayUtils.equal(
- magicNumber,
- traceBuffer.slice(0, magicNumber.length)
- );
- if (!bufferContainsMagicNumber) {
- throw TypeError("buffer doesn't contain expected magic number");
- }
- }
-
- this.decodedEntries = this.decodeTrace(traceBuffer).map((it) => this.addDefaultProtoFields(it));
-
+ private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
+ const timeStampMap = new Map<TimestampType, Timestamp[]>();
for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
const timestamps: Timestamp[] = [];
let areTimestampsValid = true;
@@ -59,9 +60,10 @@
}
if (areTimestampsValid) {
- this.timestamps.set(type, timestamps);
+ timeStampMap.set(type, timestamps);
}
}
+ return timeStampMap;
}
abstract getTraceType(): TraceType;
@@ -78,43 +80,17 @@
return this.timestamps.get(type);
}
- getEntry(index: number, timestampType: TimestampType): Promise<T> {
+ getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T> {
const entry = this.processDecodedEntry(index, timestampType, this.decodedEntries[index]);
return Promise.resolve(entry);
}
- // Add default values to the proto objects.
- private addDefaultProtoFields(protoObj: any): any {
- if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
- return protoObj;
- }
-
- for (const fieldName in protoObj.$type.fields) {
- if (Object.prototype.hasOwnProperty.call(protoObj.$type.fields, fieldName)) {
- const fieldProperties = protoObj.$type.fields[fieldName];
- const field = protoObj[fieldName];
-
- if (Array.isArray(field)) {
- field.forEach((item, _) => {
- this.addDefaultProtoFields(item);
- });
- continue;
- }
-
- if (!field) {
- protoObj[fieldName] = fieldProperties.defaultValue;
- }
-
- if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
- protoObj[fieldName] =
- fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
- continue;
- }
- this.addDefaultProtoFields(protoObj[fieldName]);
- }
- }
-
- return protoObj;
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange,
+ param?: CustomQueryParamTypeMap[Q]
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ throw new Error('Not implemented');
}
protected abstract getMagicNumber(): undefined | number[];
diff --git a/tools/winscope/src/parsers/abstract_traces_parser.ts b/tools/winscope/src/parsers/abstract_traces_parser.ts
index 266c3ba..e8531ba 100644
--- a/tools/winscope/src/parsers/abstract_traces_parser.ts
+++ b/tools/winscope/src/parsers/abstract_traces_parser.ts
@@ -14,8 +14,10 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {CustomQueryParserResultTypeMap, CustomQueryType} from 'trace/custom_query';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
export abstract class AbstractTracesParser<T> implements Parser<T> {
@@ -28,7 +30,14 @@
abstract getTraceType(): TraceType;
- abstract getEntry(index: number, timestampType: TimestampType): Promise<T>;
+ abstract getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ throw new Error('Not implemented');
+ }
abstract getLengthEntries(): number;
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/parsers/file_and_parser.ts
similarity index 76%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/parsers/file_and_parser.ts
index d2b1744..c07c663 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/parsers/file_and_parser.ts
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export class FileAndParser {
+ constructor(readonly file: TraceFile, readonly parser: Parser<object>) {}
}
diff --git a/tools/winscope/src/interfaces/user_notification_listener.ts b/tools/winscope/src/parsers/file_and_parsers.ts
similarity index 75%
copy from tools/winscope/src/interfaces/user_notification_listener.ts
copy to tools/winscope/src/parsers/file_and_parsers.ts
index d2b1744..692f2ba 100644
--- a/tools/winscope/src/interfaces/user_notification_listener.ts
+++ b/tools/winscope/src/parsers/file_and_parsers.ts
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-import {ParserError} from 'parsers/parser_factory';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
-export interface UserNotificationListener {
- onParserErrors(errors: ParserError[]): void;
+export class FileAndParsers {
+ constructor(readonly file: TraceFile, readonly parsers: Array<Parser<object>>) {}
}
diff --git a/tools/winscope/src/parsers/parser_accessibility.ts b/tools/winscope/src/parsers/parser_accessibility.ts
deleted file mode 100644
index fb30419..0000000
--- a/tools/winscope/src/parsers/parser_accessibility.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.
- */
-
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceFile} from 'trace/trace_file';
-import {TraceType} from 'trace/trace_type';
-import {AbstractParser} from './abstract_parser';
-import {AccessibilityTraceFileProto} from './proto_types';
-
-class ParserAccessibility extends AbstractParser {
- constructor(trace: TraceFile) {
- super(trace);
- this.realToElapsedTimeOffsetNs = undefined;
- }
-
- override getTraceType(): TraceType {
- return TraceType.ACCESSIBILITY;
- }
-
- override getMagicNumber(): number[] {
- return ParserAccessibility.MAGIC_NUMBER;
- }
-
- override decodeTrace(buffer: Uint8Array): any[] {
- const decoded = AccessibilityTraceFileProto.decode(buffer) as any;
- if (Object.prototype.hasOwnProperty.call(decoded, 'realToElapsedTimeOffsetNanos')) {
- this.realToElapsedTimeOffsetNs = BigInt(decoded.realToElapsedTimeOffsetNanos);
- } else {
- this.realToElapsedTimeOffsetNs = undefined;
- }
- return decoded.entry;
- }
-
- override getTimestamp(type: TimestampType, entryProto: any): undefined | Timestamp {
- if (type === TimestampType.ELAPSED) {
- return new Timestamp(type, BigInt(entryProto.elapsedRealtimeNanos));
- } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
- return new Timestamp(
- type,
- this.realToElapsedTimeOffsetNs + BigInt(entryProto.elapsedRealtimeNanos)
- );
- }
- return undefined;
- }
-
- override processDecodedEntry(index: number, timestampType: TimestampType, entryProto: any): any {
- return entryProto;
- }
-
- private realToElapsedTimeOffsetNs: undefined | bigint;
- private static readonly MAGIC_NUMBER = [0x09, 0x41, 0x31, 0x31, 0x59, 0x54, 0x52, 0x41, 0x43]; // .A11YTRAC
-}
-
-export {ParserAccessibility};
diff --git a/tools/winscope/src/parsers/parser_accessibility_test.ts b/tools/winscope/src/parsers/parser_accessibility_test.ts
deleted file mode 100644
index 9362264..0000000
--- a/tools/winscope/src/parsers/parser_accessibility_test.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.
- */
-import {UnitTestUtils} from 'test/unit/utils';
-import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
-import {TraceType} from 'trace/trace_type';
-
-describe('ParserAccessibility', () => {
- describe('trace with elapsed + real timestamp', () => {
- let parser: Parser<any>;
-
- beforeAll(async () => {
- parser = await UnitTestUtils.getParser('traces/elapsed_and_real_timestamp/Accessibility.pb');
- });
-
- it('has expected trace type', () => {
- expect(parser.getTraceType()).toEqual(TraceType.ACCESSIBILITY);
- });
-
- it('provides elapsed timestamps', () => {
- const expected = [
- new Timestamp(TimestampType.ELAPSED, 14499089524n),
- new Timestamp(TimestampType.ELAPSED, 14499599656n),
- new Timestamp(TimestampType.ELAPSED, 14953120693n),
- ];
- expect(parser.getTimestamps(TimestampType.ELAPSED)!.slice(0, 3)).toEqual(expected);
- });
-
- it('provides real timestamps', () => {
- const expected = [
- new Timestamp(TimestampType.REAL, 1659107089100052652n),
- new Timestamp(TimestampType.REAL, 1659107089100562784n),
- new Timestamp(TimestampType.REAL, 1659107089554083821n),
- ];
- expect(parser.getTimestamps(TimestampType.REAL)!.slice(0, 3)).toEqual(expected);
- });
-
- it('retrieves trace entry', async () => {
- const entry = await parser.getEntry(1, TimestampType.REAL);
- expect(BigInt(entry.elapsedRealtimeNanos)).toEqual(14499599656n);
- });
- });
-
- describe('trace with elapsed (only) timestamp', () => {
- let parser: Parser<any>;
-
- beforeAll(async () => {
- parser = await UnitTestUtils.getParser('traces/elapsed_timestamp/Accessibility.pb');
- });
-
- it('has expected trace type', () => {
- expect(parser.getTraceType()).toEqual(TraceType.ACCESSIBILITY);
- });
-
- it('provides elapsed timestamps', () => {
- expect(parser.getTimestamps(TimestampType.ELAPSED)![0]).toEqual(
- new Timestamp(TimestampType.ELAPSED, 850297444302n)
- );
- });
-
- it("doesn't provide real timestamps", () => {
- expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
- });
- });
-});
diff --git a/tools/winscope/src/parsers/parser_common_test.ts b/tools/winscope/src/parsers/parser_common_test.ts
index 6f8ec58..dd88e33 100644
--- a/tools/winscope/src/parsers/parser_common_test.ts
+++ b/tools/winscope/src/parsers/parser_common_test.ts
@@ -13,19 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {CommonTestUtils} from 'test/common/utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {ParserFactory} from './parser_factory';
describe('Parser', () => {
it('is robust to empty trace file', async () => {
- const trace = new TraceFile(await CommonTestUtils.getFixtureFile('traces/empty.pb'), undefined);
- const [parsers, errors] = await new ParserFactory().createParsers([trace]);
+ const trace = new TraceFile(await UnitTestUtils.getFixtureFile('traces/empty.pb'), undefined);
+ const parsers = await new ParserFactory().createParsers([trace]);
expect(parsers.length).toEqual(0);
});
diff --git a/tools/winscope/src/parsers/parser_eventlog.ts b/tools/winscope/src/parsers/parser_eventlog.ts
index 902e80c..ff03aca 100644
--- a/tools/winscope/src/parsers/parser_eventlog.ts
+++ b/tools/winscope/src/parsers/parser_eventlog.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import {Event, EventLogParser} from 'trace/flickerlib/common';
-import {RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {Event, EventLogParser} from 'flickerlib/common';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_eventlog_test.ts b/tools/winscope/src/parsers/parser_eventlog_test.ts
index d1834d8..5c0b1f6 100644
--- a/tools/winscope/src/parsers/parser_eventlog_test.ts
+++ b/tools/winscope/src/parsers/parser_eventlog_test.ts
@@ -15,10 +15,10 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimestampType} from 'common/time';
+import {CujEvent} from 'flickerlib/common';
import {UnitTestUtils} from 'test/unit/utils';
-import {CujEvent} from 'trace/flickerlib/common';
import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserEventLog', () => {
diff --git a/tools/winscope/src/parsers/parser_factory.ts b/tools/winscope/src/parsers/parser_factory.ts
index 577f49d..5921efb 100644
--- a/tools/winscope/src/parsers/parser_factory.ts
+++ b/tools/winscope/src/parsers/parser_factory.ts
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
+import {ProgressListener} from 'messaging/progress_listener';
+import {WinscopeError, WinscopeErrorType} from 'messaging/winscope_error';
+import {WinscopeErrorListener} from 'messaging/winscope_error_listener';
import {Parser} from 'trace/parser';
import {TraceFile} from 'trace/trace_file';
-import {TraceType} from 'trace/trace_type';
-import {ParserAccessibility} from './parser_accessibility';
+import {FileAndParser} from './file_and_parser';
import {ParserEventLog} from './parser_eventlog';
import {ParserInputMethodClients} from './parser_input_method_clients';
import {ParserInputMethodManagerService} from './parser_input_method_manager_service';
@@ -36,7 +37,6 @@
export class ParserFactory {
static readonly PARSERS = [
- ParserAccessibility,
ParserInputMethodClients,
ParserInputMethodManagerService,
ParserInputMethodService,
@@ -53,31 +53,29 @@
ParserViewCapture,
];
- private parsers = new Map<TraceType, Parser<object>>();
-
async createParsers(
traceFiles: TraceFile[],
- onProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
- ): Promise<[Array<{file: TraceFile; parser: Parser<object>}>, ParserError[]]> {
- const errors: ParserError[] = [];
-
+ progressListener?: ProgressListener,
+ errorListener?: WinscopeErrorListener
+ ): Promise<FileAndParser[]> {
const parsers = new Array<{file: TraceFile; parser: Parser<object>}>();
- if (traceFiles.length === 0) {
- errors.push(new ParserError(ParserErrorType.NO_INPUT_FILES));
- }
-
for (const [index, traceFile] of traceFiles.entries()) {
+ progressListener?.onProgressUpdate('Parsing proto files', (index / traceFiles.length) * 100);
+
let hasFoundParser = false;
for (const ParserType of ParserFactory.PARSERS) {
try {
- const parser = new ParserType(traceFile);
- await parser.parse();
+ const p = new ParserType(traceFile);
+ await p.parse();
hasFoundParser = true;
- if (this.shouldUseParser(parser, errors)) {
- this.parsers.set(parser.getTraceType(), parser);
- parsers.push({file: traceFile, parser});
+ if (p instanceof ParserViewCapture) {
+ p.getWindowParsers().forEach((subParser) =>
+ parsers.push(new FileAndParser(traceFile, subParser))
+ );
+ } else {
+ parsers.push({file: traceFile, parser: p});
}
break;
} catch (error) {
@@ -86,71 +84,11 @@
}
if (!hasFoundParser) {
- console.log(`Failed to load trace ${traceFile.file.name}`);
- errors.push(new ParserError(ParserErrorType.UNSUPPORTED_FORMAT, traceFile.getDescriptor()));
+ errorListener?.onError(
+ new WinscopeError(WinscopeErrorType.UNSUPPORTED_FILE_FORMAT, traceFile.getDescriptor())
+ );
}
-
- onProgressUpdate((100 * (index + 1)) / traceFiles.length);
}
-
- return [parsers, errors];
+ return parsers;
}
-
- private shouldUseParser(newParser: Parser<object>, errors: ParserError[]): boolean {
- const oldParser = this.parsers.get(newParser.getTraceType());
- if (!oldParser) {
- console.log(
- `Loaded trace ${newParser
- .getDescriptors()
- .join()} (trace type: ${newParser.getTraceType()})`
- );
- return true;
- }
-
- if (newParser.getLengthEntries() > oldParser.getLengthEntries()) {
- console.log(
- `Loaded trace ${newParser
- .getDescriptors()
- .join()} (trace type: ${newParser.getTraceType()}).` +
- ` Replace trace ${oldParser.getDescriptors().join()}`
- );
- errors.push(
- new ParserError(
- ParserErrorType.OVERRIDE,
- oldParser.getDescriptors().join(),
- oldParser.getTraceType()
- )
- );
- return true;
- }
-
- console.log(
- `Skipping trace ${newParser
- .getDescriptors()
- .join()} (trace type: ${newParser.getTraceType()}).` +
- ` Keep trace ${oldParser.getDescriptors().join()}`
- );
- errors.push(
- new ParserError(
- ParserErrorType.OVERRIDE,
- newParser.getDescriptors().join(),
- newParser.getTraceType()
- )
- );
- return false;
- }
-}
-
-export enum ParserErrorType {
- NO_INPUT_FILES,
- UNSUPPORTED_FORMAT,
- OVERRIDE,
-}
-
-export class ParserError {
- constructor(
- public type: ParserErrorType,
- public trace: string | undefined = undefined,
- public traceType: TraceType | undefined = undefined
- ) {}
}
diff --git a/tools/winscope/src/parsers/parser_input_method_clients.ts b/tools/winscope/src/parsers/parser_input_method_clients.ts
index 796df79..d290b23 100644
--- a/tools/winscope/src/parsers/parser_input_method_clients.ts
+++ b/tools/winscope/src/parsers/parser_input_method_clients.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_clients_test.ts b/tools/winscope/src/parsers/parser_input_method_clients_test.ts
index 265cb19..bac465d 100644
--- a/tools/winscope/src/parsers/parser_input_method_clients_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_clients_test.ts
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserInputMethodlClients', () => {
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service.ts b/tools/winscope/src/parsers/parser_input_method_manager_service.ts
index a256f54..a28cf89 100644
--- a/tools/winscope/src/parsers/parser_input_method_manager_service.ts
+++ b/tools/winscope/src/parsers/parser_input_method_manager_service.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts b/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
index 139f114..60108c5 100644
--- a/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserInputMethodManagerService', () => {
diff --git a/tools/winscope/src/parsers/parser_input_method_service.ts b/tools/winscope/src/parsers/parser_input_method_service.ts
index 5e31ea7..6f704c9 100644
--- a/tools/winscope/src/parsers/parser_input_method_service.ts
+++ b/tools/winscope/src/parsers/parser_input_method_service.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {TraceType} from 'trace/trace_type';
diff --git a/tools/winscope/src/parsers/parser_input_method_service_test.ts b/tools/winscope/src/parsers/parser_input_method_service_test.ts
index 2c0f47a..8bc451a 100644
--- a/tools/winscope/src/parsers/parser_input_method_service_test.ts
+++ b/tools/winscope/src/parsers/parser_input_method_service_test.ts
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserInputMethodService', () => {
diff --git a/tools/winscope/src/parsers/parser_protolog.ts b/tools/winscope/src/parsers/parser_protolog.ts
index 4c0c087..d2bb4a3 100644
--- a/tools/winscope/src/parsers/parser_protolog.ts
+++ b/tools/winscope/src/parsers/parser_protolog.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {FormattedLogMessage, LogMessage, UnformattedLogMessage} from 'trace/protolog';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import configJson from '../../../../../frameworks/base/data/etc/services.core.protolog.json';
diff --git a/tools/winscope/src/parsers/parser_protolog_test.ts b/tools/winscope/src/parsers/parser_protolog_test.ts
index e6d0a70..36be6f0 100644
--- a/tools/winscope/src/parsers/parser_protolog_test.ts
+++ b/tools/winscope/src/parsers/parser_protolog_test.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
import {LogMessage} from 'trace/protolog';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserProtoLog', () => {
diff --git a/tools/winscope/src/parsers/parser_screen_recording.ts b/tools/winscope/src/parsers/parser_screen_recording.ts
index 0550da5..9ae35ea 100644
--- a/tools/winscope/src/parsers/parser_screen_recording.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording.ts
@@ -15,9 +15,9 @@
*/
import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_screen_recording_legacy.ts b/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
index 1aac6ca..603a389 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_legacy.ts
@@ -15,8 +15,8 @@
*/
import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from 'common/time';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
diff --git a/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts b/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
index a595e12..e192338 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_legacy_test.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserScreenRecordingLegacy', () => {
diff --git a/tools/winscope/src/parsers/parser_screen_recording_test.ts b/tools/winscope/src/parsers/parser_screen_recording_test.ts
index 5bb830b..7e764c8 100644
--- a/tools/winscope/src/parsers/parser_screen_recording_test.ts
+++ b/tools/winscope/src/parsers/parser_screen_recording_test.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserScreenRecording', () => {
diff --git a/tools/winscope/src/parsers/parser_surface_flinger.ts b/tools/winscope/src/parsers/parser_surface_flinger.ts
index 5cb0cce..b384254 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger.ts
@@ -14,8 +14,14 @@
* limitations under the License.
*/
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
@@ -81,6 +87,33 @@
);
}
+ override customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.VSYNCID, () => {
+ const result = this.decodedEntries
+ .slice(entriesRange.start, entriesRange.end)
+ .map((entry) => {
+ return BigInt(entry.vsyncId.toString()); // convert Long to bigint
+ });
+ return Promise.resolve(result);
+ })
+ .visit(CustomQueryType.SF_LAYERS_ID_AND_NAME, () => {
+ const result: Array<{id: number; name: string}> = [];
+ this.decodedEntries
+ .slice(entriesRange.start, entriesRange.end)
+ .forEach((entry: LayerTraceEntry) => {
+ entry.layers.layers.forEach((layer: any) => {
+ result.push({id: layer.id, name: layer.name});
+ });
+ });
+ return Promise.resolve(result);
+ })
+ .getResult();
+ }
+
private realToElapsedTimeOffsetNs: undefined | bigint;
private static readonly MAGIC_NUMBER = [0x09, 0x4c, 0x59, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45]; // .LYRTRACE
}
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts b/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
index 8af0d5b..2a9f787 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger_dump_test.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
import {UnitTestUtils} from 'test/unit/utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserSurfaceFlingerDump', () => {
diff --git a/tools/winscope/src/parsers/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/parser_surface_flinger_test.ts
index c839cb9..c166bf3 100644
--- a/tools/winscope/src/parsers/parser_surface_flinger_test.ts
+++ b/tools/winscope/src/parsers/parser_surface_flinger_test.ts
@@ -13,45 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
describe('ParserSurfaceFlinger', () => {
- it('decodes layer state flags', async () => {
- const parser = (await UnitTestUtils.getParser(
- 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'
- )) as Parser<LayerTraceEntry>;
- const entry = await parser.getEntry(0, TimestampType.REAL);
-
- {
- const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 27);
- expect(layer.name).toEqual('Leaf:24:25#27');
- expect(layer.flags).toEqual(0x0);
- expect(layer.verboseFlags).toEqual('');
- }
- {
- const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 48);
- expect(layer.name).toEqual('Task=4#48');
- expect(layer.flags).toEqual(0x1);
- expect(layer.verboseFlags).toEqual('HIDDEN (0x1)');
- }
- {
- const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 77);
- expect(layer.name).toEqual('Wallpaper BBQ wrapper#77');
- expect(layer.flags).toEqual(0x100);
- expect(layer.verboseFlags).toEqual('ENABLE_BACKPRESSURE (0x100)');
- }
- });
-
describe('trace with elapsed + real timestamp', () => {
let parser: Parser<LayerTraceEntry>;
+ let trace: Trace<LayerTraceEntry>;
beforeAll(async () => {
parser = await UnitTestUtils.getParser('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb');
+ trace = new TraceBuilder().setType(TraceType.SURFACE_FLINGER).setParser(parser).build();
});
it('has expected trace type', () => {
@@ -82,6 +61,50 @@
expect(BigInt(entry.timestamp.systemUptimeNanos.toString())).toEqual(14631249355n);
expect(BigInt(entry.timestamp.unixNanos.toString())).toEqual(1659107089233029344n);
});
+
+ it('decodes layer state flags', async () => {
+ const entry = await parser.getEntry(0, TimestampType.REAL);
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 27);
+ expect(layer.name).toEqual('Leaf:24:25#27');
+ expect(layer.flags).toEqual(0x0);
+ expect(layer.verboseFlags).toEqual('');
+ }
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 48);
+ expect(layer.name).toEqual('Task=4#48');
+ expect(layer.flags).toEqual(0x1);
+ expect(layer.verboseFlags).toEqual('HIDDEN (0x1)');
+ }
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 77);
+ expect(layer.name).toEqual('Wallpaper BBQ wrapper#77');
+ expect(layer.flags).toEqual(0x100);
+ expect(layer.verboseFlags).toEqual('ENABLE_BACKPRESSURE (0x100)');
+ }
+ });
+
+ it('supports VSYNCID custom query', async () => {
+ const entries = await trace.sliceEntries(0, 3).customQuery(CustomQueryType.VSYNCID);
+ const values = entries.map((entry) => entry.getValue());
+ expect(values).toEqual([4891n, 5235n, 5748n]);
+ });
+
+ it('supports SF_LAYERS_ID_AND_NAME custom query', async () => {
+ const idAndNames = await trace
+ .sliceEntries(0, 1)
+ .customQuery(CustomQueryType.SF_LAYERS_ID_AND_NAME);
+ expect(idAndNames).toContain({id: 4, name: 'WindowedMagnification:0:31#4'});
+ expect(idAndNames).toContain({id: 5, name: 'HideDisplayCutout:0:14#5'});
+ });
+
+ it('is robust to duplicated layer ids', async () => {
+ const parser = await UnitTestUtils.getParser(
+ 'traces/elapsed_and_real_timestamp/SurfaceFlinger_with_duplicated_ids.pb'
+ );
+ const entry = await parser.getEntry(0, TimestampType.REAL);
+ expect(entry).toBeTruthy();
+ });
});
describe('trace with elapsed (only) timestamp', () => {
diff --git a/tools/winscope/src/parsers/parser_transactions.ts b/tools/winscope/src/parsers/parser_transactions.ts
index 14fc2a9..c099b09 100644
--- a/tools/winscope/src/parsers/parser_transactions.ts
+++ b/tools/winscope/src/parsers/parser_transactions.ts
@@ -14,7 +14,13 @@
* limitations under the License.
*/
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
@@ -113,6 +119,19 @@
return entryProto;
}
+ override customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.VSYNCID, async () => {
+ return this.decodedEntries.slice(entriesRange.start, entriesRange.end).map((entry) => {
+ return BigInt(entry.vsyncId.toString()); // convert Long to bigint
+ });
+ })
+ .getResult();
+ }
+
private realToElapsedTimeOffsetNs: undefined | bigint;
private static readonly MAGIC_NUMBER = [0x09, 0x54, 0x4e, 0x58, 0x54, 0x52, 0x41, 0x43, 0x45]; // .TNXTRACE
}
diff --git a/tools/winscope/src/parsers/parser_transactions_test.ts b/tools/winscope/src/parsers/parser_transactions_test.ts
index 5d4089d..3741db8 100644
--- a/tools/winscope/src/parsers/parser_transactions_test.ts
+++ b/tools/winscope/src/parsers/parser_transactions_test.ts
@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserTransactions', () => {
@@ -76,6 +78,13 @@
);
}
});
+
+ it('supports VSYNCID custom query', async () => {
+ const trace = new TraceBuilder().setType(TraceType.TRANSACTIONS).setParser(parser).build();
+ const entries = await trace.sliceEntries(0, 3).customQuery(CustomQueryType.VSYNCID);
+ const values = entries.map((entry) => entry.getValue());
+ expect(values).toEqual([1n, 2n, 3n]);
+ });
});
describe('trace with elapsed (only) timestamp', () => {
diff --git a/tools/winscope/src/parsers/parser_transitions_shell.ts b/tools/winscope/src/parsers/parser_transitions_shell.ts
index db7941b..4e0601f 100644
--- a/tools/winscope/src/parsers/parser_transitions_shell.ts
+++ b/tools/winscope/src/parsers/parser_transitions_shell.ts
@@ -14,13 +14,9 @@
* limitations under the License.
*/
-import {
- CrossPlatform,
- ShellTransitionData,
- Transition,
- WmTransitionData,
-} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {CrossPlatform, ShellTransitionData, Transition, WmTransitionData} from 'flickerlib/common';
+import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
import {ShellTransitionsTraceFileProto} from './proto_types';
@@ -29,6 +25,10 @@
private realToElapsedTimeOffsetNs: undefined | bigint;
private handlerMapping: undefined | Map<number, string>;
+ constructor(trace: TraceFile) {
+ super(trace);
+ }
+
override getTraceType(): TraceType {
return TraceType.SHELL_TRANSITION;
}
diff --git a/tools/winscope/src/parsers/parser_transitions_shell_test.ts b/tools/winscope/src/parsers/parser_transitions_shell_test.ts
index f986db7..0e85f2e 100644
--- a/tools/winscope/src/parsers/parser_transitions_shell_test.ts
+++ b/tools/winscope/src/parsers/parser_transitions_shell_test.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ShellFileParserTransitions', () => {
diff --git a/tools/winscope/src/parsers/parser_transitions_wm.ts b/tools/winscope/src/parsers/parser_transitions_wm.ts
index 0fce3f6..5d3fb14 100644
--- a/tools/winscope/src/parsers/parser_transitions_wm.ts
+++ b/tools/winscope/src/parsers/parser_transitions_wm.ts
@@ -1,3 +1,4 @@
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
import {
CrossPlatform,
ShellTransitionData,
@@ -5,8 +6,8 @@
TransitionChange,
TransitionType,
WmTransitionData,
-} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
+} from 'flickerlib/common';
+import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
import {WmTransitionsTraceFileProto} from './proto_types';
@@ -14,6 +15,10 @@
export class ParserTransitionsWm extends AbstractParser {
private realToElapsedTimeOffsetNs: undefined | bigint;
+ constructor(trace: TraceFile) {
+ super(trace);
+ }
+
override getTraceType(): TraceType {
return TraceType.WM_TRANSITION;
}
@@ -107,6 +112,20 @@
);
}
+ const startingWindowRemoveTime = null;
+ if (
+ entry.startingWindowRemoveTimeNs &&
+ BigInt(entry.startingWindowRemoveTimeNs.toString()) !== 0n
+ ) {
+ const unixNs =
+ BigInt(entry.startingWindowRemoveTimeNs.toString()) + this.realToElapsedTimeOffsetNs;
+ finishTime = CrossPlatform.timestamp.fromString(
+ entry.startingWindowRemoveTimeNs.toString(),
+ null,
+ unixNs.toString()
+ );
+ }
+
let startTransactionId = null;
if (entry.startTransactionId && BigInt(entry.startTransactionId.toString()) !== 0n) {
startTransactionId = BigInt(entry.startTransactionId.toString());
@@ -129,6 +148,7 @@
sendTime,
abortTime,
finishTime,
+ startingWindowRemoveTime,
startTransactionId?.toString(),
finishTransactionId?.toString(),
type,
diff --git a/tools/winscope/src/parsers/parser_transitions_wm_test.ts b/tools/winscope/src/parsers/parser_transitions_wm_test.ts
index 0f0cdd1..06ba000 100644
--- a/tools/winscope/src/parsers/parser_transitions_wm_test.ts
+++ b/tools/winscope/src/parsers/parser_transitions_wm_test.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('WmFileParserTransitions', () => {
diff --git a/tools/winscope/src/parsers/parser_view_capture.ts b/tools/winscope/src/parsers/parser_view_capture.ts
index d275bd9..f242d6e 100644
--- a/tools/winscope/src/parsers/parser_view_capture.ts
+++ b/tools/winscope/src/parsers/parser_view_capture.ts
@@ -14,164 +14,56 @@
* limitations under the License.
*/
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Parser} from 'trace/parser';
import {TraceFile} from 'trace/trace_file';
-import {TraceType} from 'trace/trace_type';
-import {AbstractParser} from './abstract_parser';
+import {FrameData, TraceType, WindowData} from 'trace/trace_type';
+import {ParserViewCaptureWindow} from './parser_view_capture_window';
+import {ParsingUtils} from './parsing_utils';
import {ExportedData} from './proto_types';
-/* TODO: Support multiple Windows in one file upload. */
-export class ParserViewCapture extends AbstractParser {
- private classNames: string[] = [];
- private realToElapsedTimeOffsetNanos: bigint | undefined = undefined;
- packageName: string = '';
- windowTitle: string = '';
+export class ParserViewCapture {
+ private readonly windowParsers: Array<Parser<FrameData>> = [];
- constructor(trace: TraceFile) {
- super(trace);
+ constructor(private readonly traceFile: TraceFile) {}
+
+ async parse() {
+ const traceBuffer = new Uint8Array(await this.traceFile.file.arrayBuffer());
+ ParsingUtils.throwIfMagicNumberDoesntMatch(traceBuffer, ParserViewCapture.MAGIC_NUMBER);
+
+ const exportedData = ExportedData.decode(traceBuffer) as any;
+
+ exportedData.windowData.forEach((windowData: WindowData) =>
+ this.windowParsers.push(
+ new ParserViewCaptureWindow(
+ [this.traceFile.getDescriptor()],
+ windowData.frameData,
+ ParserViewCapture.toTraceType(windowData),
+ BigInt(exportedData.realToElapsedTimeOffsetNanos),
+ exportedData.package,
+ exportedData.classname
+ )
+ )
+ );
}
- override getTraceType(): TraceType {
+ getTraceType(): TraceType {
return TraceType.VIEW_CAPTURE;
}
- override getMagicNumber(): number[] {
- return ParserViewCapture.MAGIC_NUMBER;
+ getWindowParsers(): Array<Parser<FrameData>> {
+ return this.windowParsers;
}
- override decodeTrace(buffer: Uint8Array): any[] {
- const exportedData = ExportedData.decode(buffer) as any;
- this.classNames = exportedData.classname;
- this.realToElapsedTimeOffsetNanos = BigInt(exportedData.realToElapsedTimeOffsetNanos);
- this.packageName = this.shortenAndCapitalize(exportedData.package);
-
- const firstWindowData = exportedData.windowData[0];
- this.windowTitle = this.shortenAndCapitalize(firstWindowData.title);
-
- return firstWindowData.frameData;
- }
-
- override processDecodedEntry(index: number, timestampType: TimestampType, decodedEntry: any) {
- this.formatProperties(decodedEntry.node, this.classNames);
- return decodedEntry;
- }
-
- private shortenAndCapitalize(name: string): string {
- const shortName = name.substring(name.lastIndexOf('.') + 1);
- return shortName.charAt(0).toUpperCase() + shortName.slice(1);
- }
-
- private formatProperties(root: any /* ViewNode */, classNames: string[]): any /* ViewNode */ {
- const DEPTH_MAGNIFICATION = 4;
- const VISIBLE = 0;
-
- function inner(
- node: any /* ViewNode */,
- leftShift: number,
- topShift: number,
- scaleX: number,
- scaleY: number,
- depth: number,
- isParentVisible: boolean
- ) {
- const newScaleX = scaleX * node.scaleX;
- const newScaleY = scaleY * node.scaleY;
-
- const l =
- leftShift +
- (node.left + node.translationX) * scaleX +
- (node.width * (scaleX - newScaleX)) / 2;
- const t =
- topShift +
- (node.top + node.translationY) * scaleY +
- (node.height * (scaleY - newScaleY)) / 2;
- node.boxPos = {
- left: l,
- top: t,
- width: node.width * newScaleX,
- height: node.height * newScaleY,
- };
-
- node.name = `${classNames[node.classnameIndex]}@${node.hashcode}`;
-
- node.shortName = node.name.split('.');
- node.shortName = node.shortName[node.shortName.length - 1];
-
- node.isVisible = isParentVisible && VISIBLE === node.visibility;
-
- for (let i = 0; i < node.children.length; i++) {
- inner(
- node.children[i],
- l - node.scrollX,
- t - node.scrollY,
- newScaleX,
- newScaleY,
- depth + 1,
- node.isVisible
- );
- node.children[i].parent = node;
- }
-
- // TODO: Audit these properties
- node.depth = depth * DEPTH_MAGNIFICATION;
- node.type = 'ViewNode';
- node.layerId = 0;
- node.isMissing = false;
- node.hwcCompositionType = 0;
- node.zOrderRelativeOfId = -1;
- node.isRootLayer = false;
- node.skip = null;
- node.id = node.name;
- node.stableId = node.id;
- node.equals = (other: any /* ViewNode */) => ParserViewCapture.equals(node, other);
+ private static toTraceType(windowData: WindowData): TraceType {
+ switch (windowData.title) {
+ case '.Taskbar':
+ return TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER;
+ case '.TaskbarOverlay':
+ return TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER;
+ default:
+ return TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY;
}
-
- root.scaleX = root.scaleY = 1;
- root.translationX = root.translationY = 0;
- inner(root, 0, 0, 1, 1, 0, true);
-
- root.isRootLayer = true;
- return root;
- }
-
- override getTimestamp(timestampType: TimestampType, frameData: any): undefined | Timestamp {
- return Timestamp.from(
- timestampType,
- BigInt(frameData.timestamp),
- this.realToElapsedTimeOffsetNanos
- );
}
private static readonly MAGIC_NUMBER = [0x9, 0x78, 0x65, 0x90, 0x65, 0x73, 0x82, 0x65, 0x68];
-
- /** This method is used by the tree_generator to determine if 2 nodes have equivalent properties. */
- private static equals(node: any /* ViewNode */, other: any /* ViewNode */): boolean {
- if (!node && !other) {
- return true;
- }
- if (!node || !other) {
- return false;
- }
- return (
- node.id === other.id &&
- node.name === other.name &&
- node.hashcode === other.hashcode &&
- node.left === other.left &&
- node.top === other.top &&
- node.height === other.height &&
- node.width === other.width &&
- node.elevation === other.elevation &&
- node.scaleX === other.scaleX &&
- node.scaleY === other.scaleY &&
- node.scrollX === other.scrollX &&
- node.scrollY === other.scrollY &&
- node.translationX === other.translationX &&
- node.translationY === other.translationY &&
- node.alpha === other.alpha &&
- node.visibility === other.visibility &&
- node.willNotDraw === other.willNotDraw &&
- node.clipChildren === other.clipChildren &&
- node.depth === other.depth
- );
- }
}
diff --git a/tools/winscope/src/parsers/parser_view_capture_test.ts b/tools/winscope/src/parsers/parser_view_capture_test.ts
index 9b1443a..8756db6 100644
--- a/tools/winscope/src/parsers/parser_view_capture_test.ts
+++ b/tools/winscope/src/parsers/parser_view_capture_test.ts
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -13,38 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
describe('ParserViewCapture', () => {
let parser: Parser<object>;
+ let trace: Trace<object>;
beforeAll(async () => {
parser = await UnitTestUtils.getParser(
'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc'
);
+ trace = new TraceBuilder<object>()
+ .setType(TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER)
+ .setParser(parser)
+ .build();
});
it('has expected trace type', () => {
- expect(parser.getTraceType()).toEqual(TraceType.VIEW_CAPTURE);
+ expect(parser.getTraceType()).toEqual(TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER);
});
it('provides elapsed timestamps', () => {
const expected = [
- new Timestamp(TimestampType.ELAPSED, 26231798759n),
- new Timestamp(TimestampType.ELAPSED, 26242905367n),
- new Timestamp(TimestampType.ELAPSED, 26255550549n),
+ new Timestamp(TimestampType.ELAPSED, 181114412436130n),
+ new Timestamp(TimestampType.ELAPSED, 181114421012750n),
+ new Timestamp(TimestampType.ELAPSED, 181114429047540n),
];
expect(parser.getTimestamps(TimestampType.ELAPSED)!.slice(0, 3)).toEqual(expected);
});
it('provides real timestamps', () => {
const expected = [
- new Timestamp(TimestampType.REAL, 1686674380113072216n),
- new Timestamp(TimestampType.REAL, 1686674380124178824n),
- new Timestamp(TimestampType.REAL, 1686674380136824006n),
+ new Timestamp(TimestampType.REAL, 1691692936292808460n),
+ new Timestamp(TimestampType.REAL, 1691692936301385080n),
+ new Timestamp(TimestampType.REAL, 1691692936309419870n),
];
expect(parser.getTimestamps(TimestampType.REAL)!.slice(0, 3)).toEqual(expected);
});
@@ -54,4 +62,9 @@
expect(entry.timestamp).toBeTruthy();
expect(entry.node).toBeTruthy();
});
+
+ it('supports VIEW_CAPTURE_PACKAGE_NAME custom query', async () => {
+ const packageName = await trace.customQuery(CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME);
+ expect(packageName).toEqual('com.google.android.apps.nexuslauncher');
+ });
});
diff --git a/tools/winscope/src/parsers/parser_view_capture_window.ts b/tools/winscope/src/parsers/parser_view_capture_window.ts
new file mode 100644
index 0000000..f0cd5c0
--- /dev/null
+++ b/tools/winscope/src/parsers/parser_view_capture_window.ts
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {Timestamp, TimestampType} from 'common/time';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/index_types';
+import {Parser} from 'trace/parser';
+import {FrameData, TraceType, ViewNode} from 'trace/trace_type';
+import {ParsingUtils} from './parsing_utils';
+
+export class ParserViewCaptureWindow implements Parser<FrameData> {
+ private timestamps: Map<TimestampType, Timestamp[]> = new Map<TimestampType, Timestamp[]>();
+
+ constructor(
+ private readonly descriptors: string[],
+ private readonly frameData: FrameData[],
+ private readonly traceType: TraceType,
+ private readonly realToElapsedTimeOffsetNanos: bigint,
+ private readonly packageName: string,
+ private readonly classNames: string[]
+ ) {
+ /*
+ TODO: Enable this once multiple ViewCapture Tabs becomes generic. Right now it doesn't matter since
+ the title is dependent upon the view capture type.
+
+ this.title = `${ParserViewCapture.shortenAndCapitalize(packageName)} - ${ParserViewCapture.shortenAndCapitalize(
+ windowData.title
+ )}`;
+ */
+ this.parse();
+ }
+
+ parse() {
+ this.frameData.map((it) => ParsingUtils.addDefaultProtoFields(it));
+ this.timestamps = this.decodeTimestamps();
+ }
+
+ private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
+ const timeStampMap = new Map<TimestampType, Timestamp[]>();
+ for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
+ const timestamps: Timestamp[] = [];
+ let areTimestampsValid = true;
+
+ for (const entry of this.frameData) {
+ const timestamp = Timestamp.from(
+ type,
+ BigInt(entry.timestamp),
+ this.realToElapsedTimeOffsetNanos
+ );
+ if (timestamp === undefined) {
+ areTimestampsValid = false;
+ break;
+ }
+ timestamps.push(timestamp);
+ }
+
+ if (areTimestampsValid) {
+ timeStampMap.set(type, timestamps);
+ }
+ }
+ return timeStampMap;
+ }
+
+ getTraceType(): TraceType {
+ return this.traceType;
+ }
+
+ getLengthEntries(): number {
+ return this.frameData.length;
+ }
+
+ getTimestamps(type: TimestampType): Timestamp[] | undefined {
+ return this.timestamps.get(type);
+ }
+
+ getEntry(index: number, _: TimestampType): Promise<FrameData> {
+ ParserViewCaptureWindow.formatProperties(this.frameData[index].node, this.classNames);
+ return Promise.resolve(this.frameData[index]);
+ }
+
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME, async () => {
+ return Promise.resolve(this.packageName);
+ })
+ .getResult();
+ }
+
+ getDescriptors(): string[] {
+ return this.descriptors;
+ }
+
+ private static formatProperties(root: ViewNode, classNames: string[]): ViewNode {
+ const DEPTH_MAGNIFICATION = 4;
+ const VISIBLE = 0;
+
+ function inner(
+ node: ViewNode,
+ leftShift: number,
+ topShift: number,
+ scaleX: number,
+ scaleY: number,
+ depth: number,
+ isParentVisible: boolean
+ ) {
+ const newScaleX = scaleX * node.scaleX;
+ const newScaleY = scaleY * node.scaleY;
+
+ const l =
+ leftShift +
+ (node.left + node.translationX) * scaleX +
+ (node.width * (scaleX - newScaleX)) / 2;
+ const t =
+ topShift +
+ (node.top + node.translationY) * scaleY +
+ (node.height * (scaleY - newScaleY)) / 2;
+ node.boxPos = {
+ left: l,
+ top: t,
+ width: node.width * newScaleX,
+ height: node.height * newScaleY,
+ };
+
+ node.name = `${classNames[node.classnameIndex]}@${node.hashcode}`;
+
+ node.shortName = node.name.split('.');
+ node.shortName = node.shortName[node.shortName.length - 1];
+
+ node.isVisible = isParentVisible && VISIBLE === node.visibility;
+
+ for (let i = 0; i < node.children.length; i++) {
+ inner(
+ node.children[i],
+ l - node.scrollX,
+ t - node.scrollY,
+ newScaleX,
+ newScaleY,
+ depth + 1,
+ node.isVisible
+ );
+ node.children[i].parent = node;
+ }
+
+ // TODO: Audit these properties
+ node.depth = depth * DEPTH_MAGNIFICATION;
+ node.className = node.name.substring(0, node.name.indexOf('@'));
+ node.type = 'ViewNode';
+ node.layerId = 0;
+ node.isMissing = false;
+ node.hwcCompositionType = 0;
+ node.zOrderRelativeOfId = -1;
+ node.isRootLayer = false;
+ node.skip = null;
+ node.id = node.name;
+ node.stableId = node.id;
+ node.equals = (other: ViewNode) => ParserViewCaptureWindow.equals(node, other);
+ }
+
+ root.scaleX = root.scaleY = 1;
+ root.translationX = root.translationY = 0;
+ inner(root, 0, 0, 1, 1, 0, true);
+
+ root.isRootLayer = true;
+ return root;
+ }
+
+ /** This method is used by the tree_generator to determine if 2 nodes have equivalent properties. */
+ private static equals(node: ViewNode, other: ViewNode): boolean {
+ if (!node && !other) {
+ return true;
+ }
+ if (!node || !other) {
+ return false;
+ }
+ return (
+ node.id === other.id &&
+ node.name === other.name &&
+ node.hashcode === other.hashcode &&
+ node.left === other.left &&
+ node.top === other.top &&
+ node.height === other.height &&
+ node.width === other.width &&
+ node.elevation === other.elevation &&
+ node.scaleX === other.scaleX &&
+ node.scaleY === other.scaleY &&
+ node.scrollX === other.scrollX &&
+ node.scrollY === other.scrollY &&
+ node.translationX === other.translationX &&
+ node.translationY === other.translationY &&
+ node.alpha === other.alpha &&
+ node.visibility === other.visibility &&
+ node.willNotDraw === other.willNotDraw &&
+ node.clipChildren === other.clipChildren &&
+ node.depth === other.depth
+ );
+ }
+}
diff --git a/tools/winscope/src/parsers/parser_window_manager.ts b/tools/winscope/src/parsers/parser_window_manager.ts
index 8e64f88..cd5c75f 100644
--- a/tools/winscope/src/parsers/parser_window_manager.ts
+++ b/tools/winscope/src/parsers/parser_window_manager.ts
@@ -13,14 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
+import {ParserWindowManagerUtils} from './parser_window_manager_utils';
import {WindowManagerTraceFileProto} from './proto_types';
-class ParserWindowManager extends AbstractParser {
+export class ParserWindowManager extends AbstractParser {
constructor(trace: TraceFile) {
super(trace);
this.realToElapsedTimeOffsetNs = undefined;
@@ -70,8 +77,27 @@
);
}
+ override customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE, () => {
+ const result: CustomQueryParserResultTypeMap[CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE] =
+ [];
+ this.decodedEntries
+ .slice(entriesRange.start, entriesRange.end)
+ .forEach((windowManagerTraceProto) => {
+ ParserWindowManagerUtils.parseWindowsTokenAndTitle(
+ windowManagerTraceProto?.windowManagerService?.rootWindowContainer,
+ result
+ );
+ });
+ return Promise.resolve(result);
+ })
+ .getResult();
+ }
+
private realToElapsedTimeOffsetNs: undefined | bigint;
private static readonly MAGIC_NUMBER = [0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45]; // .WINTRACE
}
-
-export {ParserWindowManager};
diff --git a/tools/winscope/src/parsers/parser_window_manager_dump.ts b/tools/winscope/src/parsers/parser_window_manager_dump.ts
index 29b0c18..52c91ea 100644
--- a/tools/winscope/src/parsers/parser_window_manager_dump.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_dump.ts
@@ -14,11 +14,18 @@
* limitations under the License.
*/
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/index_types';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {AbstractParser} from './abstract_parser';
+import {ParserWindowManagerUtils} from './parser_window_manager_utils';
import {WindowManagerServiceDumpProto} from './proto_types';
class ParserWindowManagerDump extends AbstractParser {
@@ -63,6 +70,27 @@
): WindowManagerState {
return WindowManagerState.fromProto(entryProto);
}
+
+ override customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE, () => {
+ const result: CustomQueryParserResultTypeMap[CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE] =
+ [];
+ this.decodedEntries
+ .slice(entriesRange.start, entriesRange.end)
+ .forEach((windowManagerServiceDumpProto) => {
+ ParserWindowManagerUtils.parseWindowsTokenAndTitle(
+ windowManagerServiceDumpProto?.rootWindowContainer,
+ result
+ );
+ });
+ return Promise.resolve(result);
+ })
+ .getResult();
+ }
}
export {ParserWindowManagerDump};
diff --git a/tools/winscope/src/parsers/parser_window_manager_dump_test.ts b/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
index dd6dfc2..4dd62b9 100644
--- a/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_dump_test.ts
@@ -13,17 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
describe('ParserWindowManagerDump', () => {
let parser: Parser<WindowManagerState>;
+ let trace: Trace<WindowManagerState>;
beforeAll(async () => {
parser = await UnitTestUtils.getParser('traces/dump_WindowManager.pb');
+ trace = new TraceBuilder().setType(TraceType.WINDOW_MANAGER).setParser(parser).build();
});
it('has expected trace type', () => {
@@ -45,4 +50,10 @@
expect(entry).toBeInstanceOf(WindowManagerState);
expect(BigInt(entry.timestamp.elapsedNanos.toString())).toEqual(0n);
});
+
+ it('supports WM_WINDOWS_TOKEN_AND_TITLE custom query', async () => {
+ const tokenAndTitles = await trace.customQuery(CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE);
+ expect(tokenAndTitles.length).toEqual(73);
+ expect(tokenAndTitles).toContain({token: 'cab97a6', title: 'Leaf:36:36'});
+ });
});
diff --git a/tools/winscope/src/parsers/parser_window_manager_test.ts b/tools/winscope/src/parsers/parser_window_manager_test.ts
index dd35d65..14a86fc 100644
--- a/tools/winscope/src/parsers/parser_window_manager_test.ts
+++ b/tools/winscope/src/parsers/parser_window_manager_test.ts
@@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
describe('ParserWindowManager', () => {
describe('trace with elapsed + real timestamp', () => {
let parser: Parser<WindowManagerState>;
+ let trace: Trace<WindowManagerState>;
beforeAll(async () => {
parser = await UnitTestUtils.getParser('traces/elapsed_and_real_timestamp/WindowManager.pb');
+ trace = new TraceBuilder().setType(TraceType.WINDOW_MANAGER).setParser(parser).build();
});
it('has expected trace type', () => {
@@ -60,6 +65,14 @@
const entry = await parser.getEntry(1, TimestampType.REAL);
expect(entry.name).toEqual('2022-07-29T15:04:49.999048960');
});
+
+ it('supports WM_WINDOWS_TOKEN_AND_TITLE custom query', async () => {
+ const tokenAndTitles = await trace
+ .sliceEntries(0, 1)
+ .customQuery(CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE);
+ expect(tokenAndTitles.length).toEqual(69);
+ expect(tokenAndTitles).toContain({token: 'c06766f', title: 'Leaf:36:36'});
+ });
});
describe('trace elapsed (only) timestamp', () => {
diff --git a/tools/winscope/src/parsers/parser_window_manager_utils.ts b/tools/winscope/src/parsers/parser_window_manager_utils.ts
new file mode 100644
index 0000000..196b975
--- /dev/null
+++ b/tools/winscope/src/parsers/parser_window_manager_utils.ts
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {CustomQueryParserResultTypeMap, CustomQueryType} from 'trace/custom_query';
+
+type WindowsTokenAndTitle =
+ CustomQueryParserResultTypeMap[CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE];
+
+export class ParserWindowManagerUtils {
+ private static readonly NA = 'n/a';
+
+ static parseWindowsTokenAndTitle(rootWindowContainerProto: any, result: WindowsTokenAndTitle) {
+ const token =
+ rootWindowContainerProto?.windowContainer?.identifier?.hashCode?.toString(16) ??
+ ParserWindowManagerUtils.NA;
+ const title =
+ rootWindowContainerProto?.windowContainer?.identifier?.title ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ ParserWindowManagerUtils.parseWindowContainerProto(
+ rootWindowContainerProto?.windowContainer,
+ result
+ );
+ }
+
+ private static parseWindowContainerProto(proto: any, result: WindowsTokenAndTitle) {
+ for (const windowContainerChildProto of proto?.children ?? []) {
+ if (windowContainerChildProto.activity) {
+ ParserWindowManagerUtils.parseActivityRecordProto(
+ windowContainerChildProto.activity,
+ result
+ );
+ }
+ if (windowContainerChildProto.displayArea) {
+ ParserWindowManagerUtils.parseDisplayAreaProto(
+ windowContainerChildProto.displayArea,
+ result
+ );
+ }
+ if (windowContainerChildProto.displayContent) {
+ ParserWindowManagerUtils.parseDisplayContentProto(
+ windowContainerChildProto.displayContent,
+ result
+ );
+ }
+ if (windowContainerChildProto.task) {
+ ParserWindowManagerUtils.parseTaskProto(windowContainerChildProto.task, result);
+ }
+ if (windowContainerChildProto.taskFragment) {
+ ParserWindowManagerUtils.parseTaskFragmentProto(
+ windowContainerChildProto.taskFragment,
+ result
+ );
+ }
+ if (windowContainerChildProto.window) {
+ ParserWindowManagerUtils.parseWindowStateProto(windowContainerChildProto.window, result);
+ }
+ if (windowContainerChildProto.windowContainer) {
+ ParserWindowManagerUtils.parseWindowContainerProto(
+ windowContainerChildProto.windowContainer,
+ result
+ );
+ }
+ if (windowContainerChildProto.windowToken) {
+ ParserWindowManagerUtils.parseWindowTokenProto(
+ windowContainerChildProto.windowToken,
+ result
+ );
+ }
+ }
+ }
+
+ private static parseActivityRecordProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const token = proto.windowToken?.hashCode?.toString(16) ?? ParserWindowManagerUtils.NA;
+ const title = proto.name ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ ParserWindowManagerUtils.parseWindowContainerProto(proto.windowToken?.windowContainer, result);
+ }
+
+ private static parseDisplayAreaProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const token =
+ proto.windowContainer?.identifier?.hashCode?.toString(16) ?? ParserWindowManagerUtils.NA;
+ const title = proto.name ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ ParserWindowManagerUtils.parseWindowContainerProto(proto.windowContainer, result);
+ }
+
+ private static parseDisplayContentProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const token =
+ proto.rootDisplayArea?.windowContainer?.identifier?.hashCode?.toString(16) ??
+ ParserWindowManagerUtils.NA;
+ const title = proto.displayInfo?.name ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ ParserWindowManagerUtils.parseWindowContainerProto(
+ proto.rootDisplayArea?.windowContainer,
+ result
+ );
+ }
+
+ private static parseTaskProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const token =
+ proto.taskFragment?.windowContainer?.identifier?.hashCode?.toString(16) ??
+ ParserWindowManagerUtils.NA;
+ const title =
+ proto.taskFragment?.windowContainer?.identifier?.title ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ for (const activity of proto.activities ?? []) {
+ ParserWindowManagerUtils.parseActivityRecordProto(activity, result);
+ }
+ ParserWindowManagerUtils.parseTaskFragmentProto(proto.taskFragment, result);
+ }
+
+ private static parseTaskFragmentProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ ParserWindowManagerUtils.parseWindowContainerProto(proto?.windowContainer, result);
+ }
+
+ private static parseWindowStateProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const token =
+ proto.windowContainer?.identifier?.hashCode?.toString(16) ?? ParserWindowManagerUtils.NA;
+ const title = proto.windowContainer?.identifier?.title ?? ParserWindowManagerUtils.NA;
+ result.push({token, title});
+ ParserWindowManagerUtils.parseWindowContainerProto(proto.windowContainer, result);
+ }
+
+ private static parseWindowTokenProto(proto: any, result: WindowsTokenAndTitle) {
+ if (proto === undefined) {
+ return;
+ }
+ const hash = proto.hashCode?.toString(16) ?? ParserWindowManagerUtils.NA;
+ result.push({token: hash, title: hash});
+ ParserWindowManagerUtils.parseWindowContainerProto(proto.windowContainer, result);
+ }
+}
diff --git a/tools/winscope/src/parsers/parser_window_manager_utils_test.ts b/tools/winscope/src/parsers/parser_window_manager_utils_test.ts
new file mode 100644
index 0000000..5f58e98
--- /dev/null
+++ b/tools/winscope/src/parsers/parser_window_manager_utils_test.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {UnitTestUtils} from 'test/unit/utils';
+import {CustomQueryType} from 'trace/custom_query';
+import {TraceType} from 'trace/trace_type';
+
+describe('ParserWindowManagerUtils', async () => {
+ it('parseWindowsTokenAndTitle()', async () => {
+ const trace = await UnitTestUtils.getTrace(
+ TraceType.WINDOW_MANAGER,
+ 'traces/elapsed_and_real_timestamp/WindowManager.pb'
+ );
+ const tokenAndTitles = await trace
+ .sliceEntries(0, 1)
+ .customQuery(CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE);
+
+ expect(tokenAndTitles.length).toEqual(69);
+
+ // RootWindowContainerProto
+ expect(tokenAndTitles).toContain({token: '478edff', title: 'WindowContainer'});
+ // DisplayContentProto
+ expect(tokenAndTitles).toContain({token: '1f3454e', title: 'Built-in Screen'});
+ // DisplayAreaProto
+ expect(tokenAndTitles).toContain({token: 'c06766f', title: 'Leaf:36:36'});
+ // WindowTokenProto
+ expect(tokenAndTitles).toContain({token: '509ad2f', title: '509ad2f'});
+ // WindowStateProto
+ expect(tokenAndTitles).toContain({token: 'b3b210d', title: 'ScreenDecorOverlay'});
+ // TaskProto
+ expect(tokenAndTitles).toContain({token: '7493986', title: 'Task'});
+ // ActivityRecordProto
+ expect(tokenAndTitles).toContain({
+ token: 'f7092ed',
+ title: 'com.google.android.apps.nexuslauncher/.NexusLauncherActivity',
+ });
+ });
+});
diff --git a/tools/winscope/src/parsers/parsing_utils.ts b/tools/winscope/src/parsers/parsing_utils.ts
new file mode 100644
index 0000000..3e1cd03
--- /dev/null
+++ b/tools/winscope/src/parsers/parsing_utils.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {ArrayUtils} from 'common/array_utils';
+
+export class ParsingUtils {
+ static throwIfMagicNumberDoesntMatch(traceBuffer: Uint8Array, magicNumber: number[] | undefined) {
+ if (magicNumber !== undefined) {
+ const bufferContainsMagicNumber = ArrayUtils.equal(
+ magicNumber,
+ traceBuffer.slice(0, magicNumber.length)
+ );
+ if (!bufferContainsMagicNumber) {
+ throw TypeError("buffer doesn't contain expected magic number");
+ }
+ }
+ }
+
+ // Add default values to the proto objects.
+ static addDefaultProtoFields(protoObj: any): any {
+ if (!protoObj || protoObj !== Object(protoObj) || !protoObj.$type) {
+ return protoObj;
+ }
+
+ for (const fieldName in protoObj.$type.fields) {
+ if (Object.prototype.hasOwnProperty.call(protoObj.$type.fields, fieldName)) {
+ const fieldProperties = protoObj.$type.fields[fieldName];
+ const field = protoObj[fieldName];
+
+ if (Array.isArray(field)) {
+ field.forEach((item, _) => {
+ ParsingUtils.addDefaultProtoFields(item);
+ });
+ continue;
+ }
+
+ if (!field) {
+ protoObj[fieldName] = fieldProperties.defaultValue;
+ }
+
+ if (fieldProperties.resolvedType && fieldProperties.resolvedType.valuesById) {
+ protoObj[fieldName] =
+ fieldProperties.resolvedType.valuesById[protoObj[fieldProperties.name]];
+ continue;
+ }
+ ParsingUtils.addDefaultProtoFields(protoObj[fieldName]);
+ }
+ }
+
+ return protoObj;
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/abstract_parser.ts b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
new file mode 100644
index 0000000..ef8e87d
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined, assertTrue} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'common/time';
+import {
+ CustomQueryParamTypeMap,
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+} from 'trace/custom_query';
+import {AbsoluteEntryIndex, EntriesRange} from 'trace/index_types';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+
+export abstract class AbstractParser<T> implements Parser<T> {
+ protected traceProcessor: WasmEngineProxy;
+ protected realToElapsedTimeOffsetNs?: bigint;
+ private timestamps = new Map<TimestampType, Timestamp[]>();
+ private lengthEntries = 0;
+ private traceFile: TraceFile;
+
+ constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+ this.traceFile = traceFile;
+ this.traceProcessor = traceProcessor;
+ }
+
+ async parse() {
+ const elapsedTimestamps = await this.queryElapsedTimestamps();
+ this.lengthEntries = elapsedTimestamps.length;
+ assertTrue(
+ this.lengthEntries > 0,
+ () => `Trace processor tables don't contain entries of type ${this.getTraceType()}`
+ );
+
+ this.realToElapsedTimeOffsetNs = await this.queryRealToElapsedTimeOffset(
+ assertDefined(elapsedTimestamps.at(-1))
+ );
+
+ this.timestamps.set(
+ TimestampType.ELAPSED,
+ elapsedTimestamps.map((value) => new ElapsedTimestamp(value))
+ );
+
+ this.timestamps.set(
+ TimestampType.REAL,
+ elapsedTimestamps.map(
+ (value) => new RealTimestamp(value + assertDefined(this.realToElapsedTimeOffsetNs))
+ )
+ );
+
+ if (this.lengthEntries > 0) {
+ // Make sure there are trace entries that can be parsed
+ await this.getEntry(0, TimestampType.ELAPSED);
+ }
+ }
+
+ abstract getTraceType(): TraceType;
+
+ getLengthEntries(): number {
+ return this.lengthEntries;
+ }
+
+ getTimestamps(type: TimestampType): Timestamp[] | undefined {
+ return this.timestamps.get(type);
+ }
+
+ abstract getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange,
+ param?: CustomQueryParamTypeMap[Q]
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ throw new Error('Not implemented');
+ }
+
+ getDescriptors(): string[] {
+ return [this.traceFile.getDescriptor()];
+ }
+
+ protected abstract getTableName(): string;
+
+ private async queryElapsedTimestamps(): Promise<Array<bigint>> {
+ const sql = `SELECT ts FROM ${this.getTableName()} ORDER BY id;`;
+ const result = await this.traceProcessor.query(sql).waitAllRows();
+ const timestamps: Array<bigint> = [];
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ timestamps.push(it.get('ts') as bigint);
+ }
+ return timestamps;
+ }
+
+ // Query the real-to-elapsed time offset at the specified time
+ // (timestamp parameter).
+ // The timestamp parameter must be a timestamp queried/provided by TP,
+ // otherwise the TO_REALTIME() SQL function might return invalid values.
+ private async queryRealToElapsedTimeOffset(elapsedTimestamp: bigint): Promise<bigint> {
+ const sql = `
+ SELECT TO_REALTIME(${elapsedTimestamp}) as realtime;
+ `;
+
+ const result = await this.traceProcessor.query(sql).waitAllRows();
+ assertTrue(result.numRows() === 1, () => 'Failed to query realtime timestamp');
+
+ const real = result.iter({}).get('realtime') as bigint;
+ return real - elapsedTimestamp;
+ }
+}
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/src/parsers/perfetto/abstract_parser_test.ts
similarity index 64%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to tools/winscope/src/parsers/perfetto/abstract_parser_test.ts
index dd03545..2dae9e5 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/src/parsers/perfetto/abstract_parser_test.ts
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {UnitTestUtils} from 'test/unit/utils';
-import {TracePosition} from 'trace/trace_position';
-
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
-
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+describe('Perfetto AbstractParser', () => {
+ it('fails parsing if there are no trace entries', async () => {
+ const parsers = await UnitTestUtils.getPerfettoParsers(
+ 'traces/perfetto/no_winscope_traces.perfetto-trace'
+ );
+ expect(parsers.length).toEqual(0);
+ });
+});
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts b/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts
new file mode 100644
index 0000000..a8e43ef
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_builder.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {ObjectUtils} from 'common/object_utils';
+import {StringUtils} from 'common/string_utils';
+
+export type FakeProto = any;
+
+export class FakeProtoBuilder {
+ private proto = {};
+
+ addArg(
+ key: string,
+ valueType: string,
+ intValue: bigint | undefined,
+ realValue: number | undefined,
+ stringValue: string | undefined
+ ): FakeProtoBuilder {
+ const keyCamelCase = key
+ .split('.')
+ .map((token) => {
+ return StringUtils.convertSnakeToCamelCase(token);
+ })
+ .join('.');
+ const value = this.makeValue(valueType, intValue, realValue, stringValue);
+ ObjectUtils.setProperty(this.proto, keyCamelCase, value);
+ return this;
+ }
+
+ build(): FakeProto {
+ return this.proto;
+ }
+
+ private makeValue(
+ valueType: string,
+ intValue: bigint | undefined,
+ realValue: number | undefined,
+ stringValue: string | undefined
+ ): any {
+ switch (valueType) {
+ case 'bool':
+ return Boolean(intValue);
+ case 'int':
+ return intValue;
+ case 'null':
+ return null;
+ case 'real':
+ return realValue;
+ case 'string':
+ return stringValue;
+ case 'uint':
+ return intValue;
+ default:
+ // do nothing
+ }
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts b/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts
new file mode 100644
index 0000000..f572379
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_builder_test.ts
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {fakeProtoTestJson} from 'test/protos/proto_types';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+interface Arg {
+ key: string;
+ value_type: string;
+ int_value?: bigint;
+ real_value?: number;
+ string_value?: string;
+}
+
+describe('FakeProtoBuilder', () => {
+ it('boolean', () => {
+ const args = [makeArg('a', false), makeArg('b', true)];
+ const proto = buildFakeProto(args);
+ expect(proto.a).toEqual(false);
+ expect(proto.b).toEqual(true);
+ });
+
+ it('number', () => {
+ const args = [makeArg('a', 1), makeArg('b', 10)];
+ const proto = buildFakeProto(args);
+ expect(proto.a).toEqual(1);
+ expect(proto.b).toEqual(10);
+ });
+
+ it('bigint', () => {
+ const args = [makeArg('a', 1n), makeArg('b', 10n)];
+ const proto = buildFakeProto(args);
+ expect(proto.a).toEqual(1n);
+ expect(proto.b).toEqual(10n);
+ });
+
+ it('string', () => {
+ const args = [makeArg('a', 'valuea'), makeArg('b', 'valueb')];
+ const proto = buildFakeProto(args);
+ expect(proto.a).toEqual('valuea');
+ expect(proto.b).toEqual('valueb');
+ });
+
+ it('array', () => {
+ const args = [
+ // Note: intentional random order + gap
+ makeArg('array[3]', 13),
+ makeArg('array[1]', 11),
+ makeArg('array[0]', 10),
+ ];
+ const proto = buildFakeProto(args);
+ expect(proto.array).toEqual([10, 11, undefined, 13]);
+ });
+
+ it('handles complex structure', () => {
+ const args = [
+ makeArg('a.b', false),
+ makeArg('a.numbers[0]', 10),
+ makeArg('a.numbers[1]', 11),
+ makeArg('a.objects[0].c.d', '20'),
+ makeArg('a.objects[0].c.e', '21'),
+ makeArg('a.objects[1].c', 21n),
+ ];
+ const proto = buildFakeProto(args);
+ expect(proto.a.b).toEqual(false);
+ expect(proto.a.numbers[0]).toEqual(10);
+ expect(proto.a.numbers[1]).toEqual(11);
+ expect(proto.a.objects[0].c.d).toEqual('20');
+ expect(proto.a.objects[0].c.e).toEqual('21');
+ expect(proto.a.objects[1].c).toEqual(21n);
+ });
+
+ it('converts snake_case to camelCase', () => {
+ const args = [
+ makeArg('_case_64bit', 10),
+ makeArg('case_64bit', 11),
+ makeArg('case_64bit_lsb', 12),
+ makeArg('case_64_bit', 13),
+ makeArg('case_64_bit_lsb', 14),
+ ];
+ const proto = buildFakeProto(args);
+
+ // Check it matches the snake_case to camelCase conversion performed by protobuf library (within the transformer)
+ const transformed = new FakeProtoTransformer(
+ fakeProtoTestJson,
+ 'RootMessage',
+ 'entry'
+ ).transform(proto);
+
+ expect(transformed._case_64bit).toEqual(10n);
+ expect(transformed.case_64bit).toEqual(11n);
+ expect(transformed.case_64bitLsb).toEqual(12n);
+ expect(transformed.case_64Bit).toEqual(13n);
+ expect(transformed.case_64BitLsb).toEqual(14n);
+ });
+
+ const makeArg = (key: string, value: any): Arg => {
+ if (value === null) {
+ return {key, value_type: 'null'};
+ }
+
+ switch (typeof value) {
+ case 'boolean':
+ return {key, value_type: 'bool', int_value: BigInt(value)};
+ case 'bigint':
+ return {key, value_type: 'int', int_value: value};
+ case 'number':
+ return {key, value_type: 'real', real_value: value};
+ case 'string':
+ return {key, value_type: 'string', string_value: value};
+ default:
+ throw new Error(`Unexpected value type: ${typeof value}`);
+ }
+ };
+
+ const buildFakeProto = (args: Arg[]): FakeProto => {
+ const builder = new FakeProtoBuilder();
+ args.forEach((arg) => {
+ builder.addArg(arg.key, arg.value_type, arg.int_value, arg.real_value, arg.string_value);
+ });
+ return builder.build();
+ };
+});
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts b/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts
new file mode 100644
index 0000000..61899f2
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_transformer.ts
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import * as protobuf from 'protobufjs';
+import {FakeProto} from './fake_proto_builder';
+
+export class FakeProtoTransformer {
+ private root: protobuf.Root;
+ private rootField: protobuf.Field;
+
+ constructor(protoDefinitionJson: protobuf.INamespace, parentType: string, fieldName: string) {
+ this.root = protobuf.Root.fromJSON(protoDefinitionJson);
+ this.rootField = this.root.lookupType(parentType).fields[fieldName];
+ }
+
+ transform(proto: FakeProto): FakeProto {
+ return this.transformRec(proto, this.rootField);
+ }
+
+ private transformRec(proto: FakeProto, field: protobuf.Field): FakeProto {
+ // Leaf (primitive type)
+ if (!field.repeated) {
+ switch (field.type) {
+ case 'double':
+ return Number(proto ?? 0);
+ case 'float':
+ return Number(proto ?? 0);
+ case 'int32':
+ return Number(proto ?? 0);
+ case 'uint32':
+ return Number(proto ?? 0);
+ case 'sint32':
+ return Number(proto ?? 0);
+ case 'fixed32':
+ return Number(proto ?? 0);
+ case 'sfixed32':
+ return Number(proto ?? 0);
+ case 'int64':
+ return BigInt(proto ?? 0);
+ case 'uint64':
+ return BigInt(proto ?? 0);
+ case 'sint64':
+ return BigInt(proto ?? 0);
+ case 'fixed64':
+ return BigInt(proto ?? 0);
+ case 'sfixed64':
+ return BigInt(proto ?? 0);
+ case 'string':
+ return proto;
+ case 'bool':
+ return Boolean(proto);
+ case 'bytes':
+ return proto;
+ default:
+ // do nothing
+ }
+ }
+
+ // Leaf (enum)
+ if (
+ field.resolvedType &&
+ field.resolvedType instanceof protobuf.Enum &&
+ field.resolvedType.valuesById
+ ) {
+ return field.resolvedType.valuesById[Number(proto)];
+ }
+
+ // Leaf (enum)
+ let enumType: protobuf.Enum | undefined;
+ try {
+ enumType = field.parent?.lookupEnum(field.type);
+ } catch (e) {
+ // do nothing
+ }
+ const enumId = this.tryGetEnumId(proto);
+ if (enumType && enumId !== undefined) {
+ return enumType.valuesById[enumId];
+ }
+
+ // Leaf (default value)
+ if (proto === null || proto === undefined) {
+ return field.repeated ? [] : field.defaultValue;
+ }
+
+ let protoType: protobuf.Type | undefined;
+ try {
+ protoType = this.root.lookupType(field.type);
+ } catch (e) {
+ return proto;
+ }
+
+ for (const childName in protoType.fields) {
+ if (!Object.prototype.hasOwnProperty.call(protoType.fields, childName)) {
+ continue;
+ }
+ const childField = protoType.fields[childName];
+
+ if (Array.isArray(proto[childName])) {
+ for (let i = 0; i < proto[childName].length; ++i) {
+ proto[childName][i] = this.transformRec(proto[childName][i], childField);
+ }
+ } else {
+ proto[childName] = this.transformRec(proto[childName], childField);
+ }
+ }
+
+ return proto;
+ }
+
+ private tryGetEnumId(proto: FakeProto): number | undefined {
+ if (proto === null || proto === undefined) {
+ return 0;
+ }
+
+ switch (typeof proto) {
+ case 'number':
+ return proto;
+ case 'bigint':
+ return Number(proto);
+ default:
+ return undefined;
+ }
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts b/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts
new file mode 100644
index 0000000..ae059ad
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/fake_proto_transformer_test.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {fakeProtoTestJson} from 'test/protos/proto_types';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+
+describe('FakeProtoTransformer', () => {
+ let transformer: FakeProtoTransformer;
+
+ beforeAll(() => {
+ transformer = new FakeProtoTransformer(fakeProtoTestJson, 'RootMessage', 'entry');
+ });
+
+ it('sets default value (0) of number fields', () => {
+ const proto = {};
+ const transformed = transformer.transform(proto);
+ expect(transformed.number_32bit).toEqual(0);
+ expect(transformed.number_64bit).toEqual(0n);
+ });
+
+ it('sets default value (empty array) of array fields', () => {
+ const proto = {};
+ const transformed = transformer.transform(proto);
+ expect(transformed.array).toEqual([]);
+ });
+
+ it('sets default value (id 0) of enum fields', () => {
+ const proto = {};
+ const transformed = transformer.transform(proto);
+ expect(transformed.enum0).toEqual('ENUM0_VALUE_ZERO');
+ expect(transformed.enum1).toEqual('ENUM1_VALUE_ZERO');
+ });
+
+ it('decodes enum fields', () => {
+ const proto = {
+ enum0: 1n,
+ enum1: 1n,
+ };
+ const transformed = transformer.transform(proto);
+ expect(transformed.enum0).toEqual('ENUM0_VALUE_ONE');
+ expect(transformed.enum1).toEqual('ENUM1_VALUE_ONE');
+ });
+
+ it('converts fields to number if 32-bits type', () => {
+ const proto = {
+ number_32bit: 32n,
+ };
+ const transformed = transformer.transform(proto);
+ expect(transformed.number_32bit).toEqual(32);
+ });
+
+ it('converts fields to bigint if 64-bits type', () => {
+ const proto = {
+ number_64bit: 64,
+ };
+ const transformed = transformer.transform(proto);
+ expect(transformed.number_64bit).toEqual(64n);
+ });
+});
diff --git a/tools/winscope/src/parsers/perfetto/parser_factory.ts b/tools/winscope/src/parsers/perfetto/parser_factory.ts
new file mode 100644
index 0000000..4fe9727
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_factory.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {globalConfig} from 'common/global_config';
+import {UrlUtils} from 'common/url_utils';
+import {ProgressListener} from 'messaging/progress_listener';
+import {Parser} from 'trace/parser';
+import {TraceFile} from 'trace/trace_file';
+import {initWasm, resetEngineWorker, WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {ParserSurfaceFlinger} from './parser_surface_flinger';
+import {ParserTransactions} from './parser_transactions';
+import {ParserTransitions} from './parser_transitions';
+
+export class ParserFactory {
+ private static readonly PARSERS = [ParserSurfaceFlinger, ParserTransactions, ParserTransitions];
+ private static readonly CHUNK_SIZE_BYTES = 50 * 1024 * 1024;
+ private static traceProcessor?: WasmEngineProxy;
+
+ async createParsers(
+ traceFile: TraceFile,
+ progressListener?: ProgressListener
+ ): Promise<Array<Parser<object>>> {
+ const traceProcessor = await this.initializeTraceProcessor();
+ for (
+ let chunkStart = 0;
+ chunkStart < traceFile.file.size;
+ chunkStart += ParserFactory.CHUNK_SIZE_BYTES
+ ) {
+ progressListener?.onProgressUpdate(
+ 'Loading perfetto trace...',
+ (chunkStart / traceFile.file.size) * 100
+ );
+ const chunkEnd = chunkStart + ParserFactory.CHUNK_SIZE_BYTES;
+ const data = await traceFile.file.slice(chunkStart, chunkEnd).arrayBuffer();
+ try {
+ await traceProcessor.parse(new Uint8Array(data));
+ } catch (e) {
+ console.error('Trace processor failed to parse data:', e);
+ return [];
+ }
+ }
+ await traceProcessor.notifyEof();
+
+ progressListener?.onProgressUpdate('Reading from trace processor...', undefined);
+ const parsers: Array<Parser<object>> = [];
+ for (const ParserType of ParserFactory.PARSERS) {
+ try {
+ const parser = new ParserType(traceFile, traceProcessor);
+ await parser.parse();
+ parsers.push(parser);
+ } catch (error) {
+ // skip current parser
+ }
+ }
+
+ return parsers;
+ }
+
+ private async initializeTraceProcessor(): Promise<WasmEngineProxy> {
+ if (!ParserFactory.traceProcessor) {
+ const traceProcessorRootUrl =
+ globalConfig.MODE === 'KARMA_TEST'
+ ? UrlUtils.getRootUrl() + 'base/deps_build/trace_processor/to_be_served/'
+ : UrlUtils.getRootUrl();
+ initWasm(traceProcessorRootUrl);
+ const engineId = 'random-id';
+ const enginePort = resetEngineWorker();
+ ParserFactory.traceProcessor = new WasmEngineProxy(engineId, enginePort);
+ }
+
+ await ParserFactory.traceProcessor.resetTraceProcessor({
+ cropTrackEvents: false,
+ ingestFtraceInRawTable: false,
+ analyzeTraceProtoContent: false,
+ });
+
+ return ParserFactory.traceProcessor;
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts b/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts
new file mode 100644
index 0000000..83371a6
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_surface_flinger.ts
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined, assertTrue} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {winscopeJson} from 'parsers/proto_types';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/trace';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {AbstractParser} from './abstract_parser';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+import {Utils} from './utils';
+
+export class ParserSurfaceFlinger extends AbstractParser<LayerTraceEntry> {
+ private layersSnapshotProtoTransformer = new FakeProtoTransformer(
+ winscopeJson,
+ 'WinscopeTraceData',
+ 'layersSnapshot'
+ );
+ private layerProtoTransformer = new FakeProtoTransformer(winscopeJson, 'LayersProto', 'layers');
+
+ constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+ super(traceFile, traceProcessor);
+ }
+
+ override getTraceType(): TraceType {
+ return TraceType.SURFACE_FLINGER;
+ }
+
+ override async getEntry(index: number, timestampType: TimestampType): Promise<LayerTraceEntry> {
+ let snapshotProto = await Utils.queryEntry(this.traceProcessor, this.getTableName(), index);
+ snapshotProto = this.layersSnapshotProtoTransformer.transform(snapshotProto);
+ const layerProtos = (await this.querySnapshotLayers(index)).map((layerProto) =>
+ this.layerProtoTransformer.transform(layerProto)
+ );
+ return LayerTraceEntry.fromProto(
+ layerProtos,
+ snapshotProto.displays,
+ BigInt(snapshotProto.elapsedRealtimeNanos.toString()),
+ snapshotProto.vsyncId,
+ snapshotProto.hwcBlob,
+ snapshotProto.where,
+ this.realToElapsedTimeOffsetNs,
+ timestampType === TimestampType.ELAPSED /*useElapsedTime*/,
+ snapshotProto.excludesCompositionState ?? false
+ );
+ }
+
+ override async customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.VSYNCID, async () => {
+ return Utils.queryVsyncId(this.traceProcessor, this.getTableName(), entriesRange);
+ })
+ .visit(CustomQueryType.SF_LAYERS_ID_AND_NAME, async () => {
+ const sql = `
+ SELECT DISTINCT group_concat(value) AS id_and_name FROM (
+ SELECT sfl.id AS id, args.key AS key, args.display_value AS value
+ FROM surfaceflinger_layer AS sfl
+ INNER JOIN args ON sfl.arg_set_id = args.arg_set_id
+ WHERE (args.key = 'id' OR args.key = 'name')
+ ORDER BY key
+ )
+ GROUP BY id;
+ `;
+ const querResult = await this.traceProcessor.query(sql).waitAllRows();
+ const result: CustomQueryParserResultTypeMap[CustomQueryType.SF_LAYERS_ID_AND_NAME] = [];
+ for (const it = querResult.iter({}); it.valid(); it.next()) {
+ const idAndName = it.get('id_and_name') as string;
+ const indexDelimiter = idAndName.indexOf(',');
+ assertTrue(indexDelimiter > 0, () => `Unexpected value in query result: ${idAndName}`);
+ const id = Number(idAndName.slice(0, indexDelimiter));
+ const name = idAndName.slice(indexDelimiter + 1);
+ result.push({id, name});
+ }
+ return result;
+ })
+ .getResult();
+ }
+
+ protected override getTableName(): string {
+ return 'surfaceflinger_layers_snapshot';
+ }
+
+ private async querySnapshotLayers(index: number): Promise<FakeProto[]> {
+ const layerIdToBuilder = new Map<number, FakeProtoBuilder>();
+ const getBuilder = (layerId: number) => {
+ if (!layerIdToBuilder.has(layerId)) {
+ layerIdToBuilder.set(layerId, new FakeProtoBuilder());
+ }
+ return assertDefined(layerIdToBuilder.get(layerId));
+ };
+
+ const sql = `
+ SELECT
+ sfl.snapshot_id,
+ sfl.id as layer_id,
+ args.key,
+ args.value_type,
+ args.int_value,
+ args.string_value,
+ args.real_value
+ FROM
+ surfaceflinger_layer as sfl
+ INNER JOIN args ON sfl.arg_set_id = args.arg_set_id
+ WHERE snapshot_id = ${index};
+ `;
+ const result = await this.traceProcessor.query(sql).waitAllRows();
+
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ const builder = getBuilder(it.get('layer_id') as number);
+ builder.addArg(
+ it.get('key') as string,
+ it.get('value_type') as string,
+ it.get('int_value') as bigint | undefined,
+ it.get('real_value') as number | undefined,
+ it.get('string_value') as string | undefined
+ );
+ }
+
+ const layerProtos: FakeProto[] = [];
+ layerIdToBuilder.forEach((builder) => {
+ layerProtos.push(builder.build());
+ });
+
+ return layerProtos;
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts
new file mode 100644
index 0000000..96d18c3
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_surface_flinger_test.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {Layer} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {CustomQueryType} from 'trace/custom_query';
+import {Parser} from 'trace/parser';
+import {Trace} from 'trace/trace';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserSurfaceFlinger', () => {
+ describe('valid trace', () => {
+ let parser: Parser<LayerTraceEntry>;
+ let trace: Trace<LayerTraceEntry>;
+
+ beforeAll(async () => {
+ parser = await UnitTestUtils.getPerfettoParser(
+ TraceType.SURFACE_FLINGER,
+ 'traces/perfetto/layers_trace.perfetto-trace'
+ );
+ trace = new TraceBuilder().setType(TraceType.SURFACE_FLINGER).setParser(parser).build();
+ });
+
+ it('has expected trace type', () => {
+ expect(parser.getTraceType()).toEqual(TraceType.SURFACE_FLINGER);
+ });
+
+ it('provides elapsed timestamps', () => {
+ const expected = [
+ new ElapsedTimestamp(14500282843n),
+ new ElapsedTimestamp(14631249355n),
+ new ElapsedTimestamp(15403446377n),
+ ];
+ const actual = assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3);
+ expect(actual).toEqual(expected);
+ });
+
+ it('provides real timestamps', () => {
+ const expected = [
+ new RealTimestamp(1659107089102062832n),
+ new RealTimestamp(1659107089233029344n),
+ new RealTimestamp(1659107090005226366n),
+ ];
+ const actual = assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3);
+ expect(actual).toEqual(expected);
+ });
+
+ it('formats entry timestamps (elapsed)', async () => {
+ const entry = await parser.getEntry(1, TimestampType.ELAPSED);
+ expect(entry.name).toEqual('14s631ms249355ns');
+ expect(BigInt(entry.timestamp.systemUptimeNanos.toString())).toEqual(14631249355n);
+ expect(BigInt(entry.timestamp.unixNanos.toString())).toEqual(1659107089233029344n);
+ });
+
+ it('formats entry timestamps (real)', async () => {
+ const entry = await parser.getEntry(1, TimestampType.REAL);
+ expect(entry.name).toEqual('2022-07-29T15:04:49.233029376');
+ expect(BigInt(entry.timestamp.systemUptimeNanos.toString())).toEqual(14631249355n);
+ expect(BigInt(entry.timestamp.unixNanos.toString())).toEqual(1659107089233029344n);
+ });
+
+ it('decodes layer state flags', async () => {
+ const entry = await parser.getEntry(0, TimestampType.REAL);
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 27);
+ expect(layer.name).toEqual('Leaf:24:25#27');
+ expect(layer.flags).toEqual(0x0);
+ expect(layer.verboseFlags).toEqual('');
+ }
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 48);
+ expect(layer.name).toEqual('Task=4#48');
+ expect(layer.flags).toEqual(0x1);
+ expect(layer.verboseFlags).toEqual('HIDDEN (0x1)');
+ }
+ {
+ const layer = entry.flattenedLayers.find((layer: Layer) => layer.id === 77);
+ expect(layer.name).toEqual('Wallpaper BBQ wrapper#77');
+ expect(layer.flags).toEqual(0x100);
+ expect(layer.verboseFlags).toEqual('ENABLE_BACKPRESSURE (0x100)');
+ }
+ });
+
+ it('supports VSYNCID custom query', async () => {
+ const entries = await trace.sliceEntries(0, 3).customQuery(CustomQueryType.VSYNCID);
+ const values = entries.map((entry) => entry.getValue());
+ expect(values).toEqual([4891n, 5235n, 5748n]);
+ });
+
+ it('supports SF_LAYERS_ID_AND_NAME custom query', async () => {
+ const idAndNames = await trace
+ .sliceEntries(0, 1)
+ .customQuery(CustomQueryType.SF_LAYERS_ID_AND_NAME);
+ expect(idAndNames).toContain({id: 4, name: 'WindowedMagnification:0:31#4'});
+ expect(idAndNames).toContain({id: 5, name: 'HideDisplayCutout:0:14#5'});
+ });
+ });
+
+ describe('invalid traces', () => {
+ it('is robust to duplicated layer ids', async () => {
+ const parser = await UnitTestUtils.getPerfettoParser(
+ TraceType.SURFACE_FLINGER,
+ 'traces/perfetto/layers_trace_with_duplicated_ids.perfetto-trace'
+ );
+ const entry = await parser.getEntry(0, TimestampType.REAL);
+ expect(entry).toBeTruthy();
+ });
+ });
+});
diff --git a/tools/winscope/src/parsers/perfetto/parser_transactions.ts b/tools/winscope/src/parsers/perfetto/parser_transactions.ts
new file mode 100644
index 0000000..9ecba35
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transactions.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {TimestampType} from 'common/time';
+import {TransactionsTraceFileProto, winscopeJson} from 'parsers/proto_types';
+import {
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+ VisitableParserCustomQuery,
+} from 'trace/custom_query';
+import {EntriesRange} from 'trace/index_types';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {AbstractParser} from './abstract_parser';
+import {FakeProto} from './fake_proto_builder';
+import {FakeProtoTransformer} from './fake_proto_transformer';
+import {Utils} from './utils';
+
+export class ParserTransactions extends AbstractParser<object> {
+ private protoTransformer = new FakeProtoTransformer(
+ winscopeJson,
+ 'WinscopeTraceData',
+ 'transactions'
+ );
+
+ constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+ super(traceFile, traceProcessor);
+ }
+
+ override getTraceType(): TraceType {
+ return TraceType.TRANSACTIONS;
+ }
+
+ override async getEntry(index: number, timestampType: TimestampType): Promise<object> {
+ let entryProto = await Utils.queryEntry(this.traceProcessor, this.getTableName(), index);
+ entryProto = this.protoTransformer.transform(entryProto);
+ entryProto = this.decodeWhatBitsetFields(entryProto);
+ return entryProto;
+ }
+
+ protected override getTableName(): string {
+ return 'surfaceflinger_transactions';
+ }
+
+ override async customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ return new VisitableParserCustomQuery(type)
+ .visit(CustomQueryType.VSYNCID, async () => {
+ return Utils.queryVsyncId(this.traceProcessor, this.getTableName(), entriesRange);
+ })
+ .getResult();
+ }
+
+ private decodeWhatBitsetFields(transactionTraceEntry: FakeProto): FakeProto {
+ const decodeBitset32 = (bitset: number, EnumProto: any) => {
+ return Object.keys(EnumProto).filter((key) => {
+ const value = EnumProto[key];
+ return (bitset & value) !== 0;
+ });
+ };
+
+ const concatBitsetTokens = (tokens: string[]) => {
+ if (tokens.length === 0) {
+ return '0';
+ }
+ return tokens.join(' | ');
+ };
+
+ const LayerStateChangesLsbEnum = (TransactionsTraceFileProto?.parent as any).LayerState
+ .ChangesLsb;
+ const LayerStateChangesMsbEnum = (TransactionsTraceFileProto?.parent as any).LayerState
+ .ChangesMsb;
+ const DisplayStateChangesEnum = (TransactionsTraceFileProto?.parent as any).DisplayState
+ .Changes;
+
+ transactionTraceEntry.transactions.forEach((transactionState: any) => {
+ transactionState.layerChanges.forEach((layerState: any) => {
+ layerState.what = concatBitsetTokens(
+ decodeBitset32(Number(layerState.what), LayerStateChangesLsbEnum).concat(
+ decodeBitset32(Number(layerState.what >> 32n), LayerStateChangesMsbEnum)
+ )
+ );
+ });
+
+ transactionState.displayChanges.forEach((displayState: any) => {
+ displayState.what = concatBitsetTokens(
+ decodeBitset32(Number(displayState.what), DisplayStateChangesEnum)
+ );
+ });
+ });
+
+ transactionTraceEntry?.addedDisplays?.forEach((displayState: any) => {
+ displayState.what = concatBitsetTokens(
+ decodeBitset32(Number(displayState.what), DisplayStateChangesEnum)
+ );
+ });
+
+ return transactionTraceEntry;
+ }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts b/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts
new file mode 100644
index 0000000..c2a6143
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transactions_test.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {CustomQueryType} from 'trace/custom_query';
+import {Parser} from 'trace/parser';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserTransactions', () => {
+ let parser: Parser<object>;
+
+ beforeAll(async () => {
+ parser = await UnitTestUtils.getPerfettoParser(
+ TraceType.TRANSACTIONS,
+ 'traces/perfetto/transactions_trace.perfetto-trace'
+ );
+ });
+
+ it('has expected trace type', () => {
+ expect(parser.getTraceType()).toEqual(TraceType.TRANSACTIONS);
+ });
+
+ it('provides elapsed timestamps', () => {
+ const timestamps = assertDefined(parser.getTimestamps(TimestampType.ELAPSED));
+
+ expect(timestamps.length).toEqual(712);
+
+ const expected = [
+ new ElapsedTimestamp(2450981445n),
+ new ElapsedTimestamp(2517952515n),
+ new ElapsedTimestamp(4021151449n),
+ ];
+ expect(timestamps.slice(0, 3)).toEqual(expected);
+ });
+
+ it('provides real timestamps', () => {
+ const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
+
+ expect(timestamps.length).toEqual(712);
+
+ const expected = [
+ new RealTimestamp(1659507541051480997n),
+ new RealTimestamp(1659507541118452067n),
+ new RealTimestamp(1659507542621651001n),
+ ];
+ expect(timestamps.slice(0, 3)).toEqual(expected);
+ });
+
+ it('retrieves trace entry from real timestamp', async () => {
+ const entry = await parser.getEntry(1, TimestampType.REAL);
+ expect(BigInt((entry as any).elapsedRealtimeNanos)).toEqual(2517952515n);
+ });
+
+ it('transforms fake proto built from trace processor args', async () => {
+ const entry0 = (await parser.getEntry(0, TimestampType.REAL)) as any;
+ const entry2 = (await parser.getEntry(2, TimestampType.REAL)) as any;
+
+ // Add empty arrays
+ expect(entry0.addedDisplays).toEqual([]);
+ expect(entry0.destroyedLayers).toEqual([]);
+ expect(entry0.removedDisplays).toEqual([]);
+ expect(entry0.destroyedLayerHandles).toEqual([]);
+ expect(entry0.displays).toEqual([]);
+
+ // Add default values
+ expect(entry0.transactions[1].pid).toEqual(0);
+
+ // Convert value types (bigint -> number)
+ expect(entry0.transactions[1].uid).toEqual(1003);
+
+ // Decode enum IDs
+ expect(entry0.transactions[0].layerChanges[0].dropInputMode).toEqual('NONE');
+ expect(entry2.transactions[0].layerChanges[0].bufferData.pixelFormat).toEqual(
+ 'PIXEL_FORMAT_RGBA_1010102'
+ );
+ });
+
+ it("decodes 'what' field in proto", async () => {
+ {
+ const entry = (await parser.getEntry(0, TimestampType.REAL)) as any;
+ expect(entry.transactions[0].layerChanges[0].what).toEqual('eLayerChanged');
+ expect(entry.transactions[1].layerChanges[0].what).toEqual(
+ 'eFlagsChanged | eDestinationFrameChanged'
+ );
+ }
+ {
+ const entry = (await parser.getEntry(222, TimestampType.REAL)) as any;
+ expect(entry.transactions[1].displayChanges[0].what).toEqual(
+ 'eLayerStackChanged | eDisplayProjectionChanged | eFlagsChanged'
+ );
+ }
+ });
+
+ it('supports VSYNCID custom query', async () => {
+ const trace = new TraceBuilder().setType(TraceType.TRANSACTIONS).setParser(parser).build();
+ const entries = await trace.sliceEntries(0, 3).customQuery(CustomQueryType.VSYNCID);
+ const values = entries.map((entry) => entry.getValue());
+ expect(values).toEqual([1n, 2n, 3n]);
+ });
+});
diff --git a/tools/winscope/src/parsers/perfetto/parser_transitions.ts b/tools/winscope/src/parsers/perfetto/parser_transitions.ts
new file mode 100644
index 0000000..83f6b68
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transitions.ts
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {
+ CrossPlatform,
+ ShellTransitionData,
+ Timestamp,
+ Transition,
+ TransitionChange,
+ TransitionType,
+ WmTransitionData,
+} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {AbstractParser} from './abstract_parser';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+
+export class ParserTransitions extends AbstractParser<Transition> {
+ constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+ super(traceFile, traceProcessor);
+ }
+
+ override getTraceType(): TraceType {
+ return TraceType.TRANSITION;
+ }
+
+ override async getEntry(index: number, timestampType: TimestampType): Promise<LayerTraceEntry> {
+ const transitionProto = await this.queryTransition(index);
+
+ if (this.handlerIdToName === undefined) {
+ const handlers = await this.queryHandlers();
+ this.handlerIdToName = {};
+ handlers.forEach((it) => (assertDefined(this.handlerIdToName)[it.id] = it.name));
+ }
+
+ return new Transition(
+ Number(transitionProto.id),
+ new WmTransitionData(
+ this.toTimestamp(transitionProto.createTimeNs),
+ this.toTimestamp(transitionProto.sendTimeNs),
+ this.toTimestamp(transitionProto.wmAbortTimeNs),
+ this.toTimestamp(transitionProto.finishTimeNs),
+ this.toTimestamp(transitionProto.startingWindowRemoveTimeNs),
+ transitionProto.startTransactionId.toString(),
+ transitionProto.finishTransactionId.toString(),
+ TransitionType.Companion.fromInt(Number(transitionProto.type)),
+ transitionProto.targets.map(
+ (it: any) =>
+ new TransitionChange(
+ TransitionType.Companion.fromInt(Number(it.mode)),
+ Number(it.layerId),
+ Number(it.windowId)
+ )
+ )
+ ),
+ new ShellTransitionData(
+ this.toTimestamp(transitionProto.dispatchTimeNs),
+ this.toTimestamp(transitionProto.mergeRequestTimeNs),
+ this.toTimestamp(transitionProto.mergeTimeNs),
+ this.toTimestamp(transitionProto.shellAbortTimeNs),
+ this.handlerIdToName[Number(transitionProto.handler)],
+ transitionProto.mergeTarget ? Number(transitionProto.mergeTarget) : null
+ )
+ );
+ }
+
+ private toTimestamp(n: BigInt | undefined | null): Timestamp | null {
+ if (n === undefined || n === null) {
+ return null;
+ }
+
+ const realToElapsedTimeOffsetNs = assertDefined(this.realToElapsedTimeOffsetNs);
+ const unixNs = BigInt(n.toString()) + realToElapsedTimeOffsetNs;
+
+ return CrossPlatform.timestamp.fromString(n.toString(), null, unixNs.toString());
+ }
+
+ protected override getTableName(): string {
+ return 'window_manager_shell_transitions';
+ }
+
+ private async queryTransition(index: number): Promise<FakeProto> {
+ const protoBuilder = new FakeProtoBuilder();
+
+ const sql = `
+ SELECT
+ transitions.transition_id,
+ args.key,
+ args.value_type,
+ args.int_value,
+ args.string_value,
+ args.real_value
+ FROM
+ window_manager_shell_transitions as transitions
+ INNER JOIN args ON transitions.arg_set_id = args.arg_set_id
+ WHERE transitions.id = ${index};
+ `;
+ const result = await this.traceProcessor.query(sql).waitAllRows();
+
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ protoBuilder.addArg(
+ it.get('key') as string,
+ it.get('value_type') as string,
+ it.get('int_value') as bigint | undefined,
+ it.get('real_value') as number | undefined,
+ it.get('string_value') as string | undefined
+ );
+ }
+
+ return protoBuilder.build();
+ }
+
+ private async queryHandlers(): Promise<TransitionHandler[]> {
+ const sql = 'SELECT handler_id, handler_name FROM window_manager_shell_transition_handlers;';
+ const result = await this.traceProcessor.query(sql).waitAllRows();
+
+ const handlers: TransitionHandler[] = [];
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ handlers.push({
+ id: it.get('handler_id') as number,
+ name: it.get('handler_name') as string,
+ });
+ }
+
+ return handlers;
+ }
+
+ private handlerIdToName: {[id: number]: string} | undefined = undefined;
+}
+
+interface TransitionHandler {
+ id: number;
+ name: string;
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts b/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts
new file mode 100644
index 0000000..02113d5
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_transitions_test.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {Transition, TransitionType} from 'flickerlib/common';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {Trace} from 'trace/trace';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserTransitions', () => {
+ describe('valid trace', () => {
+ let parser: Parser<Transition>;
+ let trace: Trace<Transition>;
+
+ beforeAll(async () => {
+ parser = await UnitTestUtils.getPerfettoParser(
+ TraceType.TRANSITION,
+ 'traces/perfetto/shell_transitions_trace.perfetto-trace'
+ );
+ trace = new TraceBuilder().setType(TraceType.TRANSITION).setParser(parser).build();
+ });
+
+ it('has expected trace type', () => {
+ expect(parser.getTraceType()).toEqual(TraceType.TRANSITION);
+ });
+
+ it('provides elapsed timestamps', () => {
+ const expected = [
+ new ElapsedTimestamp(479602824452n),
+ new ElapsedTimestamp(480676958445n),
+ new ElapsedTimestamp(487195167758n),
+ ];
+ const actual = assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3);
+ expect(actual).toEqual(expected);
+ });
+
+ it('provides real timestamps', () => {
+ const expected = [
+ new RealTimestamp(1700573903102738218n),
+ new RealTimestamp(1700573904176872211n),
+ new RealTimestamp(1700573910695081524n),
+ ];
+ const actual = assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3);
+ expect(actual).toEqual(expected);
+ });
+
+ it('decodes transition properties', async () => {
+ const entry = await parser.getEntry(0, TimestampType.REAL);
+
+ expect(entry.id).toEqual(32);
+ expect(entry.createTime.elapsedNanos.toString()).toEqual('479583450794');
+ expect(entry.sendTime.elapsedNanos.toString()).toEqual('479596405791');
+ expect(entry.abortTime).toEqual(null);
+ expect(entry.finishTime.elapsedNanos.toString()).toEqual('480124777862');
+ expect(entry.startingWindowRemoveTime.elapsedNanos.toString()).toEqual('479719652658');
+ expect(entry.dispatchTime.elapsedNanos.toString()).toEqual('479602824452');
+ expect(entry.mergeRequestTime).toEqual(null);
+ expect(entry.mergeTime).toEqual(null);
+ expect(entry.shellAbortTime).toEqual(null);
+ expect(entry.startTransactionId.toString()).toEqual('5811090758076');
+ expect(entry.finishTransactionId.toString()).toEqual('5811090758077');
+ expect(entry.type).toEqual(TransitionType.OPEN);
+ expect(entry.mergeTarget).toEqual(null);
+ expect(entry.handler).toEqual('com.android.wm.shell.transition.DefaultMixedHandler');
+ expect(entry.merged).toEqual(false);
+ expect(entry.played).toEqual(true);
+ expect(entry.aborted).toEqual(false);
+ expect(entry.changes.length).toEqual(2);
+ expect(entry.changes[0].layerId).toEqual(398);
+ expect(entry.changes[1].layerId).toEqual(47);
+ expect(entry.changes[0].transitMode).toEqual(TransitionType.TO_FRONT);
+ expect(entry.changes[1].transitMode).toEqual(TransitionType.TO_BACK);
+ });
+ });
+});
diff --git a/tools/winscope/src/parsers/perfetto/utils.ts b/tools/winscope/src/parsers/perfetto/utils.ts
new file mode 100644
index 0000000..1f737f1
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/utils.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {assertTrue} from 'common/assert_utils';
+import {EntriesRange} from 'trace/trace';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {FakeProto, FakeProtoBuilder} from './fake_proto_builder';
+
+export class Utils {
+ static async queryEntry(
+ traceProcessor: WasmEngineProxy,
+ tableName: string,
+ index: number
+ ): Promise<FakeProto> {
+ const sql = `
+ SELECT
+ tbl.id AS trace_entry_id,
+ args.key,
+ args.value_type,
+ args.int_value,
+ args.string_value,
+ args.real_value
+ FROM ${tableName} AS tbl
+ INNER JOIN args ON tbl.arg_set_id = args.arg_set_id
+ WHERE trace_entry_id = ${index};
+ `;
+ const result = await traceProcessor.query(sql).waitAllRows();
+
+ const builder = new FakeProtoBuilder();
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ builder.addArg(
+ it.get('key') as string,
+ it.get('value_type') as string,
+ it.get('int_value') as bigint | undefined,
+ it.get('real_value') as number | undefined,
+ it.get('string_value') as string | undefined
+ );
+ }
+ return builder.build();
+ }
+
+ static async queryVsyncId(
+ traceProcessor: WasmEngineProxy,
+ tableName: string,
+ entriesRange: EntriesRange
+ ): Promise<Array<bigint>> {
+ const sql = `
+ SELECT
+ tbl.id as entry_index,
+ args.key,
+ args.value_type,
+ args.int_value
+ FROM ${tableName} AS tbl
+ INNER JOIN args ON tbl.arg_set_id = args.arg_set_id
+ WHERE
+ entry_index BETWEEN ${entriesRange.start} AND ${entriesRange.end - 1}
+ AND args.key = 'vsync_id'
+ ORDER BY entry_index;
+ `;
+
+ const result = await traceProcessor.query(sql).waitAllRows();
+
+ const values: Array<bigint> = [];
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ const value = it.get('int_value') as bigint | undefined;
+ const valueType = it.get('value_type') as string;
+ assertTrue(
+ valueType === 'uint' || valueType === 'int',
+ () => 'expected vsyncid to have integer type'
+ );
+ values.push(value ?? -1n);
+ }
+ return values;
+ }
+}
diff --git a/tools/winscope/src/parsers/proto_types.js b/tools/winscope/src/parsers/proto_types.js
index f37f91b..0bfabfe 100644
--- a/tools/winscope/src/parsers/proto_types.js
+++ b/tools/winscope/src/parsers/proto_types.js
@@ -19,19 +19,16 @@
protobuf.util.Long = Long; // otherwise 64-bit types would be decoded as javascript number (only 53-bits precision)
protobuf.configure();
+import winscopeJson from 'external/perfetto/protos/perfetto/trace/android/winscope.proto';
import protoLogJson from 'frameworks/base/core/proto/android/internal/protolog.proto';
-import accessibilityJson from 'frameworks/base/core/proto/android/server/accessibilitytrace.proto';
import windowManagerJson from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto';
import wmTransitionsJson from 'frameworks/base/core/proto/android/server/windowmanagertransitiontrace.proto';
import inputMethodClientsJson from 'frameworks/base/core/proto/android/view/inputmethod/inputmethodeditortrace.proto';
import shellTransitionsJson from 'frameworks/base/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto';
import viewCaptureJson from 'frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto';
-import layersJson from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto';
-import transactionsJson from 'frameworks/native/services/surfaceflinger/layerproto/transactions.proto';
+import layersJson from 'protos/udc/surfaceflinger/layerstrace.proto';
+import transactionsJson from 'protos/udc/surfaceflinger/transactions.proto';
-const AccessibilityTraceFileProto = protobuf.Root.fromJSON(accessibilityJson).lookupType(
- 'com.android.server.accessibility.AccessibilityTraceFileProto'
-);
const InputMethodClientsTraceFileProto = protobuf.Root.fromJSON(inputMethodClientsJson).lookupType(
'android.view.inputmethod.InputMethodClientsTraceFileProto'
);
@@ -68,16 +65,17 @@
);
export {
- AccessibilityTraceFileProto,
+ ExportedData,
InputMethodClientsTraceFileProto,
InputMethodManagerServiceTraceFileProto,
InputMethodServiceTraceFileProto,
LayersTraceFileProto,
ProtoLogFileProto,
+ ShellTransitionsTraceFileProto,
TransactionsTraceFileProto,
WindowManagerServiceDumpProto,
WindowManagerTraceFileProto,
WmTransitionsTraceFileProto,
- ShellTransitionsTraceFileProto,
- ExportedData,
+ transactionsJson,
+ winscopeJson,
};
diff --git a/tools/winscope/src/parsers/traces_parser_cujs.ts b/tools/winscope/src/parsers/traces_parser_cujs.ts
index 4b2b7f7..959f8da 100644
--- a/tools/winscope/src/parsers/traces_parser_cujs.ts
+++ b/tools/winscope/src/parsers/traces_parser_cujs.ts
@@ -15,27 +15,24 @@
*/
import {assertDefined} from 'common/assert_utils';
-import {Cuj, EventLog, Transition} from 'trace/flickerlib/common';
-import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {Cuj, EventLog, Transition} from 'flickerlib/common';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
import {AbstractTracesParser} from './abstract_traces_parser';
-import {ParserEventLog} from './parser_eventlog';
export class TracesParserCujs extends AbstractTracesParser<Transition> {
- private readonly eventLogTrace: ParserEventLog | undefined;
+ private readonly eventLogTrace: Trace<object> | undefined;
private readonly descriptors: string[];
private decodedEntries: Cuj[] | undefined;
- constructor(parsers: Array<Parser<object>>) {
+ constructor(traces: Traces) {
super();
- const eventlogTraces = parsers.filter((it) => it.getTraceType() === TraceType.EVENT_LOG);
- if (eventlogTraces.length > 0) {
- this.eventLogTrace = eventlogTraces[0] as ParserEventLog;
- }
-
- if (this.eventLogTrace !== undefined) {
+ const eventlogTrace = traces.getTrace(TraceType.EVENT_LOG);
+ if (eventlogTrace !== undefined) {
+ this.eventLogTrace = eventlogTrace;
this.descriptors = this.eventLogTrace.getDescriptors();
} else {
this.descriptors = [];
@@ -44,14 +41,11 @@
override async parse() {
if (this.eventLogTrace === undefined) {
- throw new Error('eventLogTrace not defined');
+ throw new Error('EventLog trace not defined');
}
- const events: Event[] = [];
-
- for (let i = 0; i < this.eventLogTrace.getLengthEntries(); i++) {
- events.push(await this.eventLogTrace.getEntry(i, TimestampType.REAL));
- }
+ const eventsPromises = this.eventLogTrace.mapEntry((entry) => entry.getValue());
+ const events = await Promise.all(eventsPromises);
this.decodedEntries = new EventLog(events).cujTrace.entries;
diff --git a/tools/winscope/src/parsers/traces_parser_cujs_test.ts b/tools/winscope/src/parsers/traces_parser_cujs_test.ts
index 9541e03..3aebded 100644
--- a/tools/winscope/src/parsers/traces_parser_cujs_test.ts
+++ b/tools/winscope/src/parsers/traces_parser_cujs_test.ts
@@ -14,10 +14,10 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {Cuj} from 'flickerlib/common';
import {UnitTestUtils} from 'test/unit/utils';
-import {Cuj} from 'trace/flickerlib/common';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserCujs', () => {
diff --git a/tools/winscope/src/parsers/traces_parser_factory.ts b/tools/winscope/src/parsers/traces_parser_factory.ts
index 7eb276f..c6756d0 100644
--- a/tools/winscope/src/parsers/traces_parser_factory.ts
+++ b/tools/winscope/src/parsers/traces_parser_factory.ts
@@ -15,26 +15,26 @@
*/
import {Parser} from 'trace/parser';
+import {Traces} from 'trace/traces';
import {TracesParserCujs} from './traces_parser_cujs';
import {TracesParserTransitions} from './traces_parser_transitions';
export class TracesParserFactory {
static readonly PARSERS = [TracesParserCujs, TracesParserTransitions];
- async createParsers(parsers: Array<Parser<object>>): Promise<Array<Parser<object>>> {
- const tracesParsers: Array<Parser<object>> = [];
+ async createParsers(traces: Traces): Promise<Array<Parser<object>>> {
+ const parsers: Array<Parser<object>> = [];
for (const ParserType of TracesParserFactory.PARSERS) {
try {
- const parser = new ParserType(parsers);
+ const parser = new ParserType(traces);
await parser.parse();
- tracesParsers.push(parser);
- break;
+ parsers.push(parser);
} catch (error) {
// skip current parser
}
}
- return tracesParsers;
+ return parsers;
}
}
diff --git a/tools/winscope/src/parsers/traces_parser_transitions.ts b/tools/winscope/src/parsers/traces_parser_transitions.ts
index b364e5c..eb4b201 100644
--- a/tools/winscope/src/parsers/traces_parser_transitions.ts
+++ b/tools/winscope/src/parsers/traces_parser_transitions.ts
@@ -15,33 +15,26 @@
*/
import {assertDefined} from 'common/assert_utils';
-import {Transition, TransitionsTrace} from 'trace/flickerlib/common';
-import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {Transition, TransitionsTrace} from 'flickerlib/common';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
import {AbstractTracesParser} from './abstract_traces_parser';
export class TracesParserTransitions extends AbstractTracesParser<Transition> {
- private readonly wmTransitionTrace: Parser<object> | undefined;
- private readonly shellTransitionTrace: Parser<object> | undefined;
+ private readonly wmTransitionTrace: Trace<object> | undefined;
+ private readonly shellTransitionTrace: Trace<object> | undefined;
private readonly descriptors: string[];
private decodedEntries: Transition[] | undefined;
- constructor(parsers: Array<Parser<object>>) {
+ constructor(traces: Traces) {
super();
- const wmTransitionTraces = parsers.filter(
- (it) => it.getTraceType() === TraceType.WM_TRANSITION
- );
- if (wmTransitionTraces.length > 0) {
- this.wmTransitionTrace = wmTransitionTraces[0];
- }
- const shellTransitionTraces = parsers.filter(
- (it) => it.getTraceType() === TraceType.SHELL_TRANSITION
- );
- if (shellTransitionTraces.length > 0) {
- this.shellTransitionTrace = shellTransitionTraces[0];
- }
- if (this.wmTransitionTrace !== undefined && this.shellTransitionTrace !== undefined) {
+ const wmTransitionTrace = traces.getTrace(TraceType.WM_TRANSITION);
+ const shellTransitionTrace = traces.getTrace(TraceType.SHELL_TRANSITION);
+ if (wmTransitionTrace && shellTransitionTrace) {
+ this.wmTransitionTrace = wmTransitionTrace;
+ this.shellTransitionTrace = shellTransitionTrace;
this.descriptors = this.wmTransitionTrace
.getDescriptors()
.concat(this.shellTransitionTrace.getDescriptors());
@@ -59,17 +52,13 @@
throw new Error('Missing Shell Transition trace');
}
- const wmTransitionEntries: Transition[] = [];
- for (let index = 0; index < this.wmTransitionTrace.getLengthEntries(); index++) {
- wmTransitionEntries.push(await this.wmTransitionTrace.getEntry(index, TimestampType.REAL));
- }
+ const wmTransitionEntries: Transition[] = await Promise.all(
+ this.wmTransitionTrace.mapEntry((entry) => entry.getValue())
+ );
- const shellTransitionEntries: Transition[] = [];
- for (let index = 0; index < this.shellTransitionTrace.getLengthEntries(); index++) {
- shellTransitionEntries.push(
- await this.shellTransitionTrace.getEntry(index, TimestampType.REAL)
- );
- }
+ const shellTransitionEntries: Transition[] = await Promise.all(
+ this.shellTransitionTrace.mapEntry((entry) => entry.getValue())
+ );
const transitionsTrace = new TransitionsTrace(
wmTransitionEntries.concat(shellTransitionEntries)
diff --git a/tools/winscope/src/parsers/traces_parser_transitions_test.ts b/tools/winscope/src/parsers/traces_parser_transitions_test.ts
index 3c10a1a..7f9dbe0 100644
--- a/tools/winscope/src/parsers/traces_parser_transitions_test.ts
+++ b/tools/winscope/src/parsers/traces_parser_transitions_test.ts
@@ -14,10 +14,10 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
import {UnitTestUtils} from 'test/unit/utils';
-import {Transition} from 'trace/flickerlib/common';
import {Parser} from 'trace/parser';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceType} from 'trace/trace_type';
describe('ParserTransitions', () => {
diff --git a/tools/winscope/src/parsers/transform_utils.ts b/tools/winscope/src/parsers/transform_utils.ts
new file mode 100644
index 0000000..6a98c7e
--- /dev/null
+++ b/tools/winscope/src/parsers/transform_utils.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_utils';
+
+export class Transform {
+ constructor(public type: TransformType, public matrix: TransformMatrix) {}
+}
+
+export enum TransformType {
+ EMPTY = 0x0,
+ TRANSLATE_VAL = 0x0001,
+ ROTATE_VAL = 0x0002,
+ SCALE_VAL = 0x0004,
+ FLIP_H_VAL = 0x0100,
+ FLIP_V_VAL = 0x0200,
+ ROT_90_VAL = 0x0400,
+ ROT_INVALID_VAL = 0x8000,
+}
+
+export const EMPTY_TRANSFORM = new Transform(TransformType.EMPTY, IDENTITY_MATRIX);
+
+export class TransformUtils {
+ static isValidTransform(transform: any): boolean {
+ if (!transform) return false;
+ return transform.dsdx * transform.dtdy !== transform.dtdx * transform.dsdy;
+ }
+
+ static getTransform(transform: any, position: any): Transform {
+ const transformType = transform?.type ?? 0;
+ const x = position?.x ?? 0;
+ const y = position?.y ?? 0;
+
+ if (!transform || TransformUtils.isSimpleTransform(transformType)) {
+ return TransformUtils.getDefaultTransform(transformType, x, y);
+ }
+
+ return new Transform(transformType, {
+ dsdx: transform?.matrix.dsdx ?? 0,
+ dtdx: transform?.matrix.dtdx ?? 0,
+ tx: x,
+ dsdy: transform?.matrix.dsdy ?? 0,
+ dtdy: transform?.matrix.dtdy ?? 0,
+ ty: y,
+ });
+ }
+
+ static isSimpleRotation(transform: any): boolean {
+ return !(transform?.type
+ ? TransformUtils.isFlagSet(transform.type, TransformType.ROT_INVALID_VAL)
+ : false);
+ }
+
+ private static getDefaultTransform(type: TransformType, x: number, y: number): Transform {
+ // IDENTITY
+ if (!type) {
+ return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
+ }
+
+ // ROT_270 = ROT_90|FLIP_H|FLIP_V
+ if (
+ TransformUtils.isFlagSet(
+ type,
+ TransformType.ROT_90_VAL | TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL
+ )
+ ) {
+ return new Transform(type, {dsdx: 0, dtdx: -1, tx: x, dsdy: 1, dtdy: 0, ty: y});
+ }
+
+ // ROT_180 = FLIP_H|FLIP_V
+ if (TransformUtils.isFlagSet(type, TransformType.FLIP_V_VAL | TransformType.FLIP_H_VAL)) {
+ return new Transform(type, {dsdx: -1, dtdx: 0, tx: x, dsdy: 0, dtdy: -1, ty: y});
+ }
+
+ // ROT_90
+ if (TransformUtils.isFlagSet(type, TransformType.ROT_90_VAL)) {
+ return new Transform(type, {dsdx: 0, dtdx: 1, tx: x, dsdy: -1, dtdy: 0, ty: y});
+ }
+
+ // IDENTITY
+ if (TransformUtils.isFlagClear(type, TransformType.SCALE_VAL | TransformType.ROTATE_VAL)) {
+ return new Transform(type, {dsdx: 1, dtdx: 0, tx: x, dsdy: 0, dtdy: 1, ty: y});
+ }
+
+ throw new Error(`Unknown transform type ${type}`);
+ }
+
+ private static isFlagSet(type: number, bits: number): boolean {
+ type = type || 0;
+ return (type & bits) === bits;
+ }
+
+ private static isFlagClear(type: number, bits: number): boolean {
+ return (type & bits) === 0;
+ }
+
+ private static isSimpleTransform(type: number): boolean {
+ return TransformUtils.isFlagClear(
+ type,
+ TransformType.ROT_INVALID_VAL | TransformType.SCALE_VAL
+ );
+ }
+}
diff --git a/tools/winscope/src/test/common/file_impl.ts b/tools/winscope/src/test/common/file_impl.ts
deleted file mode 100644
index 315bbe1..0000000
--- a/tools/winscope/src/test/common/file_impl.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.
- */
-
-// This class is needed for unit tests because Node.js doesn't provide
-// an implementation of the Web API's File type
-class FileImpl {
- readonly size: number;
- readonly type: string;
- readonly name: string;
- readonly lastModified: number = 0;
- readonly webkitRelativePath: string = '';
- private readonly buffer: ArrayBuffer;
-
- constructor(buffer: ArrayBuffer, fileName: string) {
- this.buffer = buffer;
- this.size = this.buffer.byteLength;
- this.type = 'application/octet-stream';
- this.name = fileName;
- }
-
- arrayBuffer(): Promise<ArrayBuffer> {
- return new Promise<ArrayBuffer>((resolve) => {
- resolve(this.buffer);
- });
- }
-
- slice(start?: number, end?: number, contentType?: string): Blob {
- throw new Error('Not implemented!');
- }
-
- stream(): any {
- throw new Error('Not implemented!');
- }
-
- text(): Promise<string> {
- const utf8Decoder = new TextDecoder();
- const text = utf8Decoder.decode(this.buffer);
- return new Promise<string>((resolve) => {
- resolve(text);
- });
- }
-}
-
-export {FileImpl};
diff --git a/tools/winscope/src/test/common/utils.ts b/tools/winscope/src/test/common/utils.ts
deleted file mode 100644
index 8c81a63..0000000
--- a/tools/winscope/src/test/common/utils.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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.
- */
-import * as fs from 'fs';
-import * as path from 'path';
-import {FileImpl} from './file_impl';
-
-class CommonTestUtils {
- static async getFixtureFile(
- srcFilename: string,
- dstFilename: string = srcFilename
- ): Promise<File> {
- const buffer = CommonTestUtils.loadFixture(srcFilename);
- return new FileImpl(buffer, dstFilename) as unknown as File;
- }
-
- static loadFixture(filename: string): ArrayBuffer {
- return fs.readFileSync(CommonTestUtils.getFixturePath(filename));
- }
-
- static getFixturePath(filename: string): string {
- if (path.isAbsolute(filename)) {
- return filename;
- }
- return path.join(CommonTestUtils.getProjectRootPath(), 'src/test/fixtures', filename);
- }
-
- static getProjectRootPath(): string {
- let root = __dirname;
- while (path.basename(root) !== 'winscope') {
- root = path.dirname(root);
- }
- return root;
- }
-}
-
-export {CommonTestUtils};
diff --git a/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts b/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
index 32b9383..ac5cdb1 100644
--- a/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
+++ b/tools/winscope/src/test/e2e/cross_tool_protocol_test.ts
@@ -24,7 +24,7 @@
beforeAll(async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
- await browser.manage().timeouts().implicitlyWait(15000);
+ await browser.manage().timeouts().implicitlyWait(20000);
await E2eTestUtils.checkServerIsUp('Remote tool mock', E2eTestUtils.REMOTE_TOOL_MOCK_URL);
await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
});
diff --git a/tools/winscope/src/test/e2e/upload_traces_test.ts b/tools/winscope/src/test/e2e/upload_traces_test.ts
index cbb7bdb..16d0bc2 100644
--- a/tools/winscope/src/test/e2e/upload_traces_test.ts
+++ b/tools/winscope/src/test/e2e/upload_traces_test.ts
@@ -57,6 +57,10 @@
expect(text).toContain('Transactions');
expect(text).toContain('Transitions');
+ // Should be merged into a single Transitions trace
+ expect(text.includes('WM Transitions')).toBeFalsy();
+ expect(text.includes('Shell Transitions')).toBeFalsy();
+
expect(text).toContain('wm_log.winscope');
expect(text).toContain('ime_trace_service.winscope');
expect(text).toContain('ime_trace_managerservice.winscope');
diff --git a/tools/winscope/src/test/e2e/utils.ts b/tools/winscope/src/test/e2e/utils.ts
index 685d94b..2276e26 100644
--- a/tools/winscope/src/test/e2e/utils.ts
+++ b/tools/winscope/src/test/e2e/utils.ts
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as path from 'path';
import {browser, by, element} from 'protractor';
-import {CommonTestUtils} from '../common/utils';
-class E2eTestUtils extends CommonTestUtils {
+class E2eTestUtils {
static readonly WINSCOPE_URL = 'http://localhost:8080';
static readonly REMOTE_TOOL_MOCK_URL = 'http://localhost:8081';
@@ -28,14 +28,6 @@
}
}
- static async uploadFixture(...paths: string[]) {
- const inputFile = element(by.css('input[type="file"]'));
-
- // Uploading multiple files is not properly supported but
- // chrome handles file paths joined with new lines
- await inputFile.sendKeys(paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'));
- }
-
static async clickViewTracesButton() {
const button = element(by.css('.load-btn'));
await button.click();
@@ -48,6 +40,29 @@
await closeButton.click();
}
}
+
+ static async uploadFixture(...paths: string[]) {
+ const inputFile = element(by.css('input[type="file"]'));
+
+ // Uploading multiple files is not properly supported but
+ // chrome handles file paths joined with new lines
+ await inputFile.sendKeys(paths.map((it) => E2eTestUtils.getFixturePath(it)).join('\n'));
+ }
+
+ static getFixturePath(filename: string): string {
+ if (path.isAbsolute(filename)) {
+ return filename;
+ }
+ return path.join(E2eTestUtils.getProjectRootPath(), 'src/test/fixtures', filename);
+ }
+
+ private static getProjectRootPath(): string {
+ let root = __dirname;
+ while (path.basename(root) !== 'winscope') {
+ root = path.dirname(root);
+ }
+ return root;
+ }
}
export {E2eTestUtils};
diff --git a/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts b/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
index ba88871..7611e0b 100644
--- a/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
+++ b/tools/winscope/src/test/e2e/viewer_surface_flinger_test.ts
@@ -17,13 +17,22 @@
import {E2eTestUtils} from './utils';
describe('Viewer SurfaceFlinger', () => {
- beforeAll(async () => {
- browser.manage().timeouts().implicitlyWait(1000);
+ beforeEach(async () => {
+ browser.manage().timeouts().implicitlyWait(5000);
await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
await browser.get(E2eTestUtils.WINSCOPE_URL);
});
- it('processes trace and renders view', async () => {
+ it('processes perfetto trace and renders view', async () => {
+ await E2eTestUtils.uploadFixture('traces/perfetto/layers_trace.perfetto-trace');
+ await E2eTestUtils.closeSnackBarIfNeeded();
+ await E2eTestUtils.clickViewTracesButton();
+
+ const viewerPresent = await element(by.css('viewer-surface-flinger')).isPresent();
+ expect(viewerPresent).toBeTruthy();
+ });
+
+ it('processes legacy trace and renders view', async () => {
await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb');
await E2eTestUtils.closeSnackBarIfNeeded();
await E2eTestUtils.clickViewTracesButton();
diff --git a/tools/winscope/src/test/e2e/viewer_transactions_test.ts b/tools/winscope/src/test/e2e/viewer_transactions_test.ts
index f2eaf9f..7f1df74 100644
--- a/tools/winscope/src/test/e2e/viewer_transactions_test.ts
+++ b/tools/winscope/src/test/e2e/viewer_transactions_test.ts
@@ -17,12 +17,27 @@
import {E2eTestUtils} from './utils';
describe('Viewer Transactions', () => {
- beforeAll(async () => {
- browser.manage().timeouts().implicitlyWait(1000);
+ beforeEach(async () => {
+ browser.manage().timeouts().implicitlyWait(5000);
await E2eTestUtils.checkServerIsUp('Winscope', E2eTestUtils.WINSCOPE_URL);
await browser.get(E2eTestUtils.WINSCOPE_URL);
});
- it('processes trace and renders view', async () => {
+
+ it('processes perfetto trace and renders view', async () => {
+ await E2eTestUtils.uploadFixture('traces/perfetto/transactions_trace.perfetto-trace');
+ await E2eTestUtils.closeSnackBarIfNeeded();
+ await E2eTestUtils.clickViewTracesButton();
+
+ const isViewerRendered = await element(by.css('viewer-transactions')).isPresent();
+ expect(isViewerRendered).toBeTruthy();
+
+ const isFirstEntryRendered = await element(
+ by.css('viewer-transactions .scroll .entry')
+ ).isPresent();
+ expect(isFirstEntryRendered).toBeTruthy();
+ });
+
+ it('processes legacy trace and renders view', async () => {
await E2eTestUtils.uploadFixture('traces/elapsed_and_real_timestamp/Transactions.pb');
await E2eTestUtils.closeSnackBarIfNeeded();
await E2eTestUtils.clickViewTracesButton();
diff --git a/tools/winscope/src/test/fixtures/corrupted_archive.zip b/tools/winscope/src/test/fixtures/corrupted_archive.zip
new file mode 100644
index 0000000..c710b0a
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/corrupted_archive.zip
Binary files differ
diff --git "a/tools/winscope/src/test/fixtures/traces/SF_trace&\050*_with:_illegal+_characters.pb" "b/tools/winscope/src/test/fixtures/traces/SF_trace&\050*_with:_illegal+_characters.pb"
new file mode 100644
index 0000000..cf2d1a8
--- /dev/null
+++ "b/tools/winscope/src/test/fixtures/traces/SF_trace&\050*_with:_illegal+_characters.pb"
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/Accessibility.pb b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/Accessibility.pb
deleted file mode 100644
index 6780f22..0000000
--- a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/Accessibility.pb
+++ /dev/null
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/SurfaceFlinger_with_duplicated_ids.pb b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/SurfaceFlinger_with_duplicated_ids.pb
new file mode 100644
index 0000000..7c07bfb
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/SurfaceFlinger_with_duplicated_ids.pb
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
index f8ed4d6..649f540 100644
--- a/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
+++ b/tools/winscope/src/test/fixtures/traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/elapsed_timestamp/Accessibility.pb b/tools/winscope/src/test/fixtures/traces/elapsed_timestamp/Accessibility.pb
deleted file mode 100644
index 5414994..0000000
--- a/tools/winscope/src/test/fixtures/traces/elapsed_timestamp/Accessibility.pb
+++ /dev/null
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace
new file mode 100644
index 0000000..6117142
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace_with_duplicated_ids.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace_with_duplicated_ids.perfetto-trace
new file mode 100644
index 0000000..3424bdd
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/layers_trace_with_duplicated_ids.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace
new file mode 100644
index 0000000..abd266b
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/no_winscope_traces.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace
new file mode 100644
index 0000000..3625318
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/shell_transitions_trace.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace
new file mode 100644
index 0000000..fb45404
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/transactions_trace.perfetto-trace
Binary files differ
diff --git a/tools/winscope/src/test/protos/fake_proto_test.proto b/tools/winscope/src/test/protos/fake_proto_test.proto
new file mode 100644
index 0000000..b551705
--- /dev/null
+++ b/tools/winscope/src/test/protos/fake_proto_test.proto
@@ -0,0 +1,31 @@
+syntax = "proto2";
+
+package winscope.test;
+
+// Message used for testing the fake proto machinery
+message RootMessage {
+ optional Entry entry = 1;
+}
+
+enum Enum0 {
+ ENUM0_VALUE_ZERO = 0;
+ ENUM0_VALUE_ONE = 1;
+}
+
+message Entry {
+ enum Enum1 {
+ ENUM1_VALUE_ZERO = 0;
+ ENUM1_VALUE_ONE = 1;
+ }
+ optional Enum0 enum0 = 1;
+ optional Enum1 enum1 = 2;
+ repeated int32 array = 3;
+ optional int32 number_32bit = 4;
+ optional int64 number_64bit = 5;
+
+ optional int64 _case_64bit = 6;
+ optional int64 case_64bit = 7;
+ optional int64 case_64bit_lsb = 8;
+ optional int64 case_64_bit = 9;
+ optional int64 case_64_bit_lsb = 10;
+}
diff --git a/tools/winscope/src/interfaces/trace_position_update_emitter.ts b/tools/winscope/src/test/protos/proto_types.js
similarity index 66%
copy from tools/winscope/src/interfaces/trace_position_update_emitter.ts
copy to tools/winscope/src/test/protos/proto_types.js
index dd03545..2f6dcc6 100644
--- a/tools/winscope/src/interfaces/trace_position_update_emitter.ts
+++ b/tools/winscope/src/test/protos/proto_types.js
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import Long from 'long';
+import * as protobuf from 'protobufjs';
-import {TracePosition} from 'trace/trace_position';
+protobuf.util.Long = Long; // otherwise 64-bit types would be decoded as javascript number (only 53-bits precision)
+protobuf.configure();
-export type OnTracePositionUpdate = (position: TracePosition) => Promise<void>;
+import fakeProtoTestJson from 'src/test/protos/fake_proto_test.proto';
-export interface TracePositionUpdateEmitter {
- setOnTracePositionUpdate(callback: OnTracePositionUpdate): void;
-}
+export {fakeProtoTestJson};
diff --git a/tools/winscope/src/test/unit/layer_builder.ts b/tools/winscope/src/test/unit/layer_builder.ts
index 540a66e..1fb84a6 100644
--- a/tools/winscope/src/test/unit/layer_builder.ts
+++ b/tools/winscope/src/test/unit/layer_builder.ts
@@ -16,13 +16,14 @@
import {
ActiveBuffer,
+ Color,
EMPTY_COLOR,
EMPTY_RECT,
EMPTY_RECTF,
EMPTY_TRANSFORM,
Layer,
LayerProperties,
-} from 'trace/flickerlib/common';
+} from 'flickerlib/common';
class LayerBuilder {
setFlags(value: number): LayerBuilder {
@@ -30,34 +31,31 @@
return this;
}
+ setColor(color: Color): LayerBuilder {
+ this.color = color;
+ return this;
+ }
+
build(): Layer {
const properties = new LayerProperties(
null /* visibleRegion */,
new ActiveBuffer(0, 0, 0, 0),
this.flags,
EMPTY_RECTF /* bounds */,
- EMPTY_COLOR,
+ this.color,
false /* isOpaque */,
- 0 /* shadowRadius */,
- 0 /* cornerRadius */,
- 'type' /* type */,
+ 1 /* shadowRadius */,
+ 1 /* cornerRadius */,
EMPTY_RECTF /* screenBounds */,
EMPTY_TRANSFORM /* transform */,
- EMPTY_RECTF /* sourceBounds */,
0 /* effectiveScalingMode */,
EMPTY_TRANSFORM /* bufferTransform */,
0 /* hwcCompositionType */,
- EMPTY_RECTF /* hwcCrop */,
- EMPTY_RECT /* hwcFrame */,
- 0 /* backgroundBlurRadius */,
+ 1 /* backgroundBlurRadius */,
EMPTY_RECT /* crop */,
false /* isRelativeOf */,
-1 /* zOrderRelativeOfId */,
0 /* stackId */,
- EMPTY_TRANSFORM /* requestedTransform */,
- EMPTY_COLOR /* requestedColor */,
- EMPTY_RECTF /* cornerRadiusCrop */,
- EMPTY_TRANSFORM /* inputTransform */,
null /* inputRegion */
);
@@ -66,12 +64,13 @@
0 /* id */,
-1 /*parentId */,
0 /* z */,
- 0 /* currFrame */,
+ '0' /* currFrameString */,
properties
);
}
private flags = 0;
+ private color = EMPTY_COLOR;
}
export {LayerBuilder};
diff --git a/tools/winscope/src/test/unit/trace_builder.ts b/tools/winscope/src/test/unit/trace_builder.ts
index e4edb55..6e9d661 100644
--- a/tools/winscope/src/test/unit/trace_builder.ts
+++ b/tools/winscope/src/test/unit/trace_builder.ts
@@ -14,18 +14,20 @@
* limitations under the License.
*/
+import {Timestamp, TimestampType} from 'common/time';
+import {CustomQueryParserResultTypeMap, CustomQueryType} from 'trace/custom_query';
import {FrameMap} from 'trace/frame_map';
import {FrameMapBuilder} from 'trace/frame_map_builder';
import {AbsoluteEntryIndex, AbsoluteFrameIndex, EntriesRange} from 'trace/index_types';
import {Parser} from 'trace/parser';
import {ParserMock} from 'trace/parser_mock';
-import {Timestamp, TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {TraceType} from 'trace/trace_type';
export class TraceBuilder<T> {
private type = TraceType.SURFACE_FLINGER;
private parser?: Parser<T>;
+ private parserCustomQueryResult = new Map<CustomQueryType, {}>();
private entries?: T[];
private timestamps?: Timestamp[];
private timestampType = TimestampType.REAL;
@@ -74,8 +76,11 @@
return this;
}
- setDescriptors(descriptors: string[]): TraceBuilder<T> {
- this.descriptors = descriptors;
+ setParserCustomQueryResult<Q extends CustomQueryType>(
+ type: Q,
+ result: CustomQueryParserResultTypeMap[Q]
+ ): TraceBuilder<T> {
+ this.parserCustomQueryResult.set(type, result);
return this;
}
@@ -88,10 +93,11 @@
start: 0,
end: this.parser.getLengthEntries(),
};
- const trace = Trace.newInitializedTrace<T>(
+ const trace = new Trace<T>(
this.type,
this.parser,
this.descriptors,
+ undefined,
this.timestampType,
entriesRange
);
@@ -121,7 +127,7 @@
throw new Error('Entries and timestamps arrays must have the same length');
}
- return new ParserMock(this.timestamps, this.entries);
+ return new ParserMock(this.timestamps, this.entries, this.parserCustomQueryResult);
}
private createTimestamps(entries: T[]): Timestamp[] {
diff --git a/tools/winscope/src/test/unit/trace_utils.ts b/tools/winscope/src/test/unit/trace_utils.ts
index 2f5b882..d8a9914 100644
--- a/tools/winscope/src/test/unit/trace_utils.ts
+++ b/tools/winscope/src/test/unit/trace_utils.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {Timestamp} from 'trace/timestamp';
+import {Timestamp} from 'common/time';
import {AbsoluteFrameIndex, Trace} from 'trace/trace';
export class TraceUtils {
diff --git a/tools/winscope/src/test/unit/traces_builder.ts b/tools/winscope/src/test/unit/traces_builder.ts
index a7c8a97..49a9936 100644
--- a/tools/winscope/src/test/unit/traces_builder.ts
+++ b/tools/winscope/src/test/unit/traces_builder.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp} from 'common/time';
import {FrameMap} from 'trace/frame_map';
-import {Timestamp} from 'trace/timestamp';
import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
import {TraceBuilder} from './trace_builder';
diff --git a/tools/winscope/src/test/unit/utils.ts b/tools/winscope/src/test/unit/utils.ts
index ba450bd..db02edd 100644
--- a/tools/winscope/src/test/unit/utils.ts
+++ b/tools/winscope/src/test/unit/utils.ts
@@ -14,42 +14,99 @@
* limitations under the License.
*/
+import {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
+import {UrlUtils} from 'common/url_utils';
+import {LayerTraceEntry, WindowManagerState} from 'flickerlib/common';
import {ParserFactory} from 'parsers/parser_factory';
+import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory';
import {TracesParserFactory} from 'parsers/traces_parser_factory';
-import {CommonTestUtils} from 'test/common/utils';
-import {LayerTraceEntry, WindowManagerState} from 'trace/flickerlib/common';
import {Parser} from 'trace/parser';
-import {TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
import {TraceFile} from 'trace/trace_file';
-import {TraceType} from 'trace/trace_type';
+import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
+import {TraceBuilder} from './trace_builder';
-class UnitTestUtils extends CommonTestUtils {
- static async getTraceFromFile(filename: string): Promise<Trace<object>> {
- const parser = await UnitTestUtils.getParser(filename);
+class UnitTestUtils {
+ static async getFixtureFile(
+ srcFilename: string,
+ dstFilename: string = srcFilename
+ ): Promise<File> {
+ const url = UrlUtils.getRootUrl() + 'base/src/test/fixtures/' + srcFilename;
+ const response = await fetch(url);
+ expect(response.ok).toBeTrue();
+ const blob = await response.blob();
+ const file = new File([blob], dstFilename);
+ return file;
+ }
- const trace = Trace.newUninitializedTrace(parser);
- trace.init(
- parser.getTimestamps(TimestampType.REAL) !== undefined
- ? TimestampType.REAL
- : TimestampType.ELAPSED
- );
- return trace;
+ static async getTrace<T extends TraceType>(type: T, filename: string): Promise<Trace<T>> {
+ const legacyParsers = await UnitTestUtils.getParsers(filename);
+ expect(legacyParsers.length).toBeLessThanOrEqual(1);
+ if (legacyParsers.length === 1) {
+ expect(legacyParsers[0].getTraceType()).toEqual(type);
+ return new TraceBuilder<T>()
+ .setType(type)
+ .setParser(legacyParsers[0] as unknown as Parser<T>)
+ .build();
+ }
+
+ const perfettoParsers = await UnitTestUtils.getPerfettoParsers(filename);
+ expect(perfettoParsers.length).toEqual(1);
+ expect(perfettoParsers[0].getTraceType()).toEqual(type);
+ return new TraceBuilder<T>()
+ .setType(type)
+ .setParser(perfettoParsers[0] as unknown as Parser<T>)
+ .build();
}
static async getParser(filename: string): Promise<Parser<object>> {
- const file = new TraceFile(await CommonTestUtils.getFixtureFile(filename), undefined);
- const [parsers, errors] = await new ParserFactory().createParsers([file]);
- expect(parsers.length).toEqual(1);
- return parsers[0].parser;
+ const parsers = await UnitTestUtils.getParsers(filename);
+ expect(parsers.length)
+ .withContext(`Should have been able to create a parser for ${filename}`)
+ .toBeGreaterThanOrEqual(1);
+ return parsers[0];
+ }
+
+ static async getParsers(filename: string): Promise<Array<Parser<object>>> {
+ const file = new TraceFile(await UnitTestUtils.getFixtureFile(filename), undefined);
+ const fileAndParsers = await new ParserFactory().createParsers([file]);
+ return fileAndParsers.map((fileAndParser) => {
+ return fileAndParser.parser;
+ });
+ }
+
+ static async getPerfettoParser<T extends TraceType>(
+ traceType: T,
+ fixturePath: string
+ ): Promise<Parser<TraceEntryTypeMap[T]>> {
+ const parsers = await UnitTestUtils.getPerfettoParsers(fixturePath);
+ const parser = assertDefined(parsers.find((parser) => parser.getTraceType() === traceType));
+ return parser as Parser<TraceEntryTypeMap[T]>;
+ }
+
+ static async getPerfettoParsers(fixturePath: string): Promise<Array<Parser<object>>> {
+ const file = await UnitTestUtils.getFixtureFile(fixturePath);
+ const traceFile = new TraceFile(file);
+ return await new PerfettoParserFactory().createParsers(traceFile);
}
static async getTracesParser(filenames: string[]): Promise<Parser<object>> {
- const parsers = await Promise.all(
+ const parsersArray = await Promise.all(
filenames.map((filename) => UnitTestUtils.getParser(filename))
);
- const tracesParsers = await new TracesParserFactory().createParsers(parsers);
- expect(tracesParsers.length).toEqual(1);
+
+ const traces = new Traces();
+ parsersArray.forEach((parser) => {
+ const trace = Trace.fromParser(parser, TimestampType.REAL);
+ traces.setTrace(parser.getTraceType(), trace);
+ });
+
+ const tracesParsers = await new TracesParserFactory().createParsers(traces);
+ expect(tracesParsers.length)
+ .withContext(`Should have been able to create a traces parser for [${filenames.join()}]`)
+ .toEqual(1);
return tracesParsers[0];
}
diff --git a/tools/winscope/src/test/utils.ts b/tools/winscope/src/test/utils.ts
new file mode 100644
index 0000000..5632e50
--- /dev/null
+++ b/tools/winscope/src/test/utils.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {ComponentFixture, flush} from '@angular/core/testing';
+
+export function dispatchMouseEvent(
+ source: Node,
+ type: string,
+ screenX: number,
+ screenY: number,
+ clientX: number,
+ clientY: number
+) {
+ const event = document.createEvent('MouseEvent');
+
+ event.initMouseEvent(
+ type,
+ true /* canBubble */,
+ false /* cancelable */,
+ window /* view */,
+ 0 /* detail */,
+ screenX /* screenX */,
+ screenY /* screenY */,
+ clientX /* clientX */,
+ clientY /* clientY */,
+ false /* ctrlKey */,
+ false /* altKey */,
+ false /* shiftKey */,
+ false /* metaKey */,
+ 0 /* button */,
+ null /* relatedTarget */
+ );
+ Object.defineProperty(event, 'buttons', {get: () => 1});
+
+ source.dispatchEvent(event);
+}
+
+export function dragElement<T>(
+ fixture: ComponentFixture<T>,
+ target: Element,
+ x: number,
+ y: number
+) {
+ const {left, top} = target.getBoundingClientRect();
+
+ dispatchMouseEvent(target, 'mousedown', left, top, 0, 0);
+ fixture.detectChanges();
+ flush();
+ dispatchMouseEvent(document, 'mousemove', left + 1, top + 0, 1, y);
+ fixture.detectChanges();
+ flush();
+ dispatchMouseEvent(document, 'mousemove', left + x, top + y, x, y);
+ fixture.detectChanges();
+ flush();
+ dispatchMouseEvent(document, 'mouseup', left + x, top + y, x, y);
+ fixture.detectChanges();
+
+ flush();
+}
+
+export async function waitToBeCalled(spy: jasmine.Spy, times: number = 1, timeout = 10000) {
+ return new Promise<void>((resolve, reject) => {
+ let called = 0;
+ spy.and.callThrough().and.callFake(() => {
+ called++;
+ if (called === times) {
+ resolve();
+ }
+ });
+
+ setTimeout(() => reject(`not called ${times} times within ${timeout}ms`), timeout);
+ });
+}
diff --git a/tools/winscope/src/trace/custom_query.ts b/tools/winscope/src/trace/custom_query.ts
new file mode 100644
index 0000000..cebfbb3
--- /dev/null
+++ b/tools/winscope/src/trace/custom_query.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+import {RelativeEntryIndex, TraceEntryEager} from './trace';
+
+export enum CustomQueryType {
+ SF_LAYERS_ID_AND_NAME,
+ VIEW_CAPTURE_PACKAGE_NAME,
+ VSYNCID,
+ WM_WINDOWS_TOKEN_AND_TITLE,
+}
+
+export class ProcessParserResult {
+ static [CustomQueryType.SF_LAYERS_ID_AND_NAME]<T>(
+ parserResult: CustomQueryParserResultTypeMap[CustomQueryType.SF_LAYERS_ID_AND_NAME]
+ ): CustomQueryResultTypeMap<T>[CustomQueryType.SF_LAYERS_ID_AND_NAME] {
+ return parserResult;
+ }
+
+ static [CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME]<T>(
+ parserResult: CustomQueryParserResultTypeMap[CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME]
+ ): CustomQueryResultTypeMap<T>[CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME] {
+ return parserResult;
+ }
+
+ static [CustomQueryType.VSYNCID]<T>(
+ parserResult: CustomQueryParserResultTypeMap[CustomQueryType.VSYNCID],
+ makeTraceEntry: (index: RelativeEntryIndex, vsyncId: bigint) => TraceEntryEager<T, bigint>
+ ): CustomQueryResultTypeMap<T>[CustomQueryType.VSYNCID] {
+ return parserResult.map((vsyncId, index) => {
+ return makeTraceEntry(index, vsyncId);
+ });
+ }
+
+ static [CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE]<T>(
+ parserResult: CustomQueryParserResultTypeMap[CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE]
+ ): CustomQueryResultTypeMap<T>[CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE] {
+ return parserResult;
+ }
+}
+
+export interface CustomQueryParamTypeMap {
+ [CustomQueryType.SF_LAYERS_ID_AND_NAME]: never;
+ [CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME]: never;
+ [CustomQueryType.VSYNCID]: never;
+ [CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE]: never;
+}
+
+export interface CustomQueryParserResultTypeMap {
+ [CustomQueryType.SF_LAYERS_ID_AND_NAME]: Array<{id: number; name: string}>;
+ [CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME]: string;
+ [CustomQueryType.VSYNCID]: Array<bigint>;
+ [CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE]: Array<{token: string; title: string}>;
+}
+
+export interface CustomQueryResultTypeMap<T> {
+ [CustomQueryType.SF_LAYERS_ID_AND_NAME]: Array<{id: number; name: string}>;
+ [CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME]: string;
+ [CustomQueryType.VSYNCID]: Array<TraceEntryEager<T, bigint>>;
+ [CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE]: Array<{token: string; title: string}>;
+}
+
+export class VisitableParserCustomQuery<Q extends CustomQueryType> {
+ private readonly type: CustomQueryType;
+ private result: Promise<CustomQueryParserResultTypeMap[Q]> | undefined;
+
+ constructor(type: Q) {
+ this.type = type;
+ }
+
+ visit<R extends CustomQueryType>(
+ type: R,
+ visitor: () => Promise<CustomQueryParserResultTypeMap[R]>
+ ): VisitableParserCustomQuery<Q> {
+ if (type !== this.type) {
+ return this;
+ }
+ this.result = visitor() as Promise<CustomQueryParserResultTypeMap[Q]>;
+ return this;
+ }
+
+ getResult(): Promise<CustomQueryParserResultTypeMap[Q]> {
+ if (this.result === undefined) {
+ throw new Error(
+ `No result available. Looks like custom query (type: ${this.type}) is not implemented!`
+ );
+ }
+ return this.result;
+ }
+}
diff --git a/tools/winscope/src/trace/flickerlib/common.js b/tools/winscope/src/trace/flickerlib/common.js
deleted file mode 100644
index 5c39b99..0000000
--- a/tools/winscope/src/trace/flickerlib/common.js
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-// Imports all the compiled common Flicker library classes and exports them
-// as clean es6 modules rather than having them be commonjs modules
-
-// WM
-const WindowManagerTrace = require('flicker').android.tools.common.traces.wm.WindowManagerTrace;
-const WindowManagerState = require('flicker').android.tools.common.traces.wm.WindowManagerState;
-const WindowManagerTraceEntryBuilder =
- require('flicker').android.tools.common.traces.wm.WindowManagerTraceEntryBuilder;
-const Activity = require('flicker').android.tools.common.traces.wm.Activity;
-const Configuration = require('flicker').android.tools.common.traces.wm.Configuration;
-const ConfigurationContainer =
- require('flicker').android.tools.common.traces.wm.ConfigurationContainer;
-const DisplayArea = require('flicker').android.tools.common.traces.wm.DisplayArea;
-const DisplayContent = require('flicker').android.tools.common.traces.wm.DisplayContent;
-const DisplayCutout = require('flicker').android.tools.common.traces.wm.DisplayCutout;
-const KeyguardControllerState =
- require('flicker').android.tools.common.traces.wm.KeyguardControllerState;
-const RootWindowContainer = require('flicker').android.tools.common.traces.wm.RootWindowContainer;
-const Task = require('flicker').android.tools.common.traces.wm.Task;
-const TaskFragment = require('flicker').android.tools.common.traces.wm.TaskFragment;
-const WindowConfiguration = require('flicker').android.tools.common.traces.wm.WindowConfiguration;
-const WindowContainer = require('flicker').android.tools.common.traces.wm.WindowContainer;
-const WindowLayoutParams = require('flicker').android.tools.common.traces.wm.WindowLayoutParams;
-const WindowManagerPolicy = require('flicker').android.tools.common.traces.wm.WindowManagerPolicy;
-const WindowState = require('flicker').android.tools.common.traces.wm.WindowState;
-const WindowToken = require('flicker').android.tools.common.traces.wm.WindowToken;
-
-// SF
-const HwcCompositionType =
- require('flicker').android.tools.common.traces.surfaceflinger.HwcCompositionType;
-const Layer = require('flicker').android.tools.common.traces.surfaceflinger.Layer;
-const LayerProperties =
- require('flicker').android.tools.common.traces.surfaceflinger.LayerProperties;
-const LayerTraceEntry =
- require('flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntry;
-const LayerTraceEntryBuilder =
- require('flicker').android.tools.common.traces.surfaceflinger.LayerTraceEntryBuilder;
-const LayersTrace = require('flicker').android.tools.common.traces.surfaceflinger.LayersTrace;
-const Transform = require('flicker').android.tools.common.traces.surfaceflinger.Transform;
-const Display = require('flicker').android.tools.common.traces.surfaceflinger.Display;
-const Region = require('flicker').android.tools.common.datatypes.Region;
-
-// Event Log
-const EventLog = require('flicker').android.tools.common.traces.events.EventLog;
-const CujEvent = require('flicker').android.tools.common.traces.events.CujEvent;
-const CujType = require('flicker').android.tools.common.traces.events.CujType;
-const Event = require('flicker').android.tools.common.traces.events.Event;
-const FlickerEvent = require('flicker').android.tools.common.traces.events.FlickerEvent;
-const FocusEvent = require('flicker').android.tools.common.traces.events.FocusEvent;
-const EventLogParser = require('flicker').android.tools.common.parsers.events.EventLogParser;
-const CujTrace = require('flicker').android.tools.common.parsers.events.CujTrace;
-const Cuj = require('flicker').android.tools.common.parsers.events.Cuj;
-
-// Transitions
-const Transition = require('flicker').android.tools.common.traces.wm.Transition;
-const TransitionType = require('flicker').android.tools.common.traces.wm.TransitionType;
-const TransitionChange = require('flicker').android.tools.common.traces.wm.TransitionChange;
-const TransitionsTrace = require('flicker').android.tools.common.traces.wm.TransitionsTrace;
-const ShellTransitionData = require('flicker').android.tools.common.traces.wm.ShellTransitionData;
-const WmTransitionData = require('flicker').android.tools.common.traces.wm.WmTransitionData;
-
-// Common
-const Size = require('flicker').android.tools.common.datatypes.Size;
-const ActiveBuffer = require('flicker').android.tools.common.datatypes.ActiveBuffer;
-const Color = require('flicker').android.tools.common.datatypes.Color;
-const Insets = require('flicker').android.tools.common.datatypes.Insets;
-const Matrix33 = require('flicker').android.tools.common.datatypes.Matrix33;
-const PlatformConsts = require('flicker').android.tools.common.PlatformConsts;
-const Rotation = require('flicker').android.tools.common.Rotation;
-const Point = require('flicker').android.tools.common.datatypes.Point;
-const PointF = require('flicker').android.tools.common.datatypes.PointF;
-const Rect = require('flicker').android.tools.common.datatypes.Rect;
-const RectF = require('flicker').android.tools.common.datatypes.RectF;
-const WindowingMode = require('flicker').android.tools.common.traces.wm.WindowingMode;
-const CrossPlatform = require('flicker').android.tools.common.CrossPlatform;
-const TimestampFactory = require('flicker').android.tools.common.TimestampFactory;
-
-const EMPTY_SIZE = Size.Companion.EMPTY;
-const EMPTY_BUFFER = ActiveBuffer.Companion.EMPTY;
-const EMPTY_COLOR = Color.Companion.EMPTY;
-const EMPTY_INSETS = Insets.Companion.EMPTY;
-const EMPTY_RECT = Rect.Companion.EMPTY;
-const EMPTY_RECTF = RectF.Companion.EMPTY;
-const EMPTY_POINT = Point.Companion.EMPTY;
-const EMPTY_POINTF = PointF.Companion.EMPTY;
-const EMPTY_MATRIX33 = Matrix33.Companion.identity(0, 0);
-const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
-
-function toSize(proto) {
- if (proto == null) {
- return EMPTY_SIZE;
- }
- const width = proto.width ?? proto.w ?? 0;
- const height = proto.height ?? proto.h ?? 0;
- if (width || height) {
- return new Size(width, height);
- }
- return EMPTY_SIZE;
-}
-
-function toActiveBuffer(proto) {
- const width = proto?.width ?? 0;
- const height = proto?.height ?? 0;
- const stride = proto?.stride ?? 0;
- const format = proto?.format ?? 0;
-
- if (width || height || stride || format) {
- return new ActiveBuffer(width, height, stride, format);
- }
- return EMPTY_BUFFER;
-}
-
-function toColor(proto, hasAlpha = true) {
- if (proto == null) {
- return EMPTY_COLOR;
- }
- const r = proto.r ?? 0;
- const g = proto.g ?? 0;
- const b = proto.b ?? 0;
- let a = proto.a;
- if (a === null && !hasAlpha) {
- a = 1;
- }
- if (r || g || b || a) {
- return new Color(r, g, b, a);
- }
- return EMPTY_COLOR;
-}
-
-function toPoint(proto) {
- if (proto == null) {
- return null;
- }
- const x = proto.x ?? 0;
- const y = proto.y ?? 0;
- if (x || y) {
- return new Point(x, y);
- }
- return EMPTY_POINT;
-}
-
-function toPointF(proto) {
- if (proto == null) {
- return null;
- }
- const x = proto.x ?? 0;
- const y = proto.y ?? 0;
- if (x || y) {
- return new PointF(x, y);
- }
- return EMPTY_POINTF;
-}
-
-function toInsets(proto) {
- if (proto == null) {
- return EMPTY_INSETS;
- }
-
- const left = proto?.left ?? 0;
- const top = proto?.top ?? 0;
- const right = proto?.right ?? 0;
- const bottom = proto?.bottom ?? 0;
- if (left || top || right || bottom) {
- return new Insets(left, top, right, bottom);
- }
- return EMPTY_INSETS;
-}
-
-function toCropRect(proto) {
- if (proto == null) return EMPTY_RECT;
-
- const right = proto.right || 0;
- const left = proto.left || 0;
- const bottom = proto.bottom || 0;
- const top = proto.top || 0;
-
- // crop (0,0) (-1,-1) means no crop
- if (right == -1 && left == 0 && bottom == -1 && top == 0) EMPTY_RECT;
-
- if (right - left <= 0 || bottom - top <= 0) return EMPTY_RECT;
-
- return Rect.Companion.from(left, top, right, bottom);
-}
-
-function toRect(proto) {
- if (proto == null) {
- return EMPTY_RECT;
- }
-
- const left = proto?.left ?? 0;
- const top = proto?.top ?? 0;
- const right = proto?.right ?? 0;
- const bottom = proto?.bottom ?? 0;
- if (left || top || right || bottom) {
- return new Rect(left, top, right, bottom);
- }
- return EMPTY_RECT;
-}
-
-function toRectF(proto) {
- if (proto == null) {
- return EMPTY_RECTF;
- }
-
- const left = proto?.left ?? 0;
- const top = proto?.top ?? 0;
- const right = proto?.right ?? 0;
- const bottom = proto?.bottom ?? 0;
- if (left || top || right || bottom) {
- return new RectF(left, top, right, bottom);
- }
- return EMPTY_RECTF;
-}
-
-function toRegion(proto) {
- if (proto == null) {
- return null;
- }
-
- const rects = [];
- for (let x = 0; x < proto.rect.length; x++) {
- const rect = proto.rect[x];
- const parsedRect = toRect(rect);
- rects.push(parsedRect);
- }
-
- return new Region(rects);
-}
-
-function toTransform(proto) {
- if (proto == null) {
- return EMPTY_TRANSFORM;
- }
- const dsdx = proto.dsdx ?? 0;
- const dtdx = proto.dtdx ?? 0;
- const tx = proto.tx ?? 0;
- const dsdy = proto.dsdy ?? 0;
- const dtdy = proto.dtdy ?? 0;
- const ty = proto.ty ?? 0;
-
- if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
- const matrix = new Matrix33(dsdx, dtdx, tx, dsdy, dtdy, ty);
- return new Transform(proto.type ?? 0, matrix);
- }
-
- if (proto.type) {
- return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
- }
- return EMPTY_TRANSFORM;
-}
-
-export {
- Activity,
- Configuration,
- ConfigurationContainer,
- DisplayArea,
- DisplayContent,
- KeyguardControllerState,
- DisplayCutout,
- RootWindowContainer,
- Task,
- TaskFragment,
- WindowConfiguration,
- WindowContainer,
- WindowState,
- WindowToken,
- WindowLayoutParams,
- WindowManagerPolicy,
- WindowManagerTrace,
- WindowManagerState,
- WindowManagerTraceEntryBuilder,
- // SF
- HwcCompositionType,
- Layer,
- LayerProperties,
- LayerTraceEntry,
- LayerTraceEntryBuilder,
- LayersTrace,
- Transform,
- Matrix33,
- Display,
- // Eventlog
- EventLog,
- CujEvent,
- CujType,
- Event,
- FlickerEvent,
- FocusEvent,
- EventLogParser,
- CujTrace,
- Cuj,
- // Transitions
- Transition,
- TransitionType,
- TransitionChange,
- TransitionsTrace,
- ShellTransitionData,
- WmTransitionData,
- // Common
- Size,
- ActiveBuffer,
- Color,
- Insets,
- PlatformConsts,
- Point,
- Rect,
- RectF,
- Region,
- Rotation,
- WindowingMode,
- CrossPlatform,
- TimestampFactory,
- // Service
- toSize,
- toActiveBuffer,
- toColor,
- toCropRect,
- toInsets,
- toPoint,
- toPointF,
- toRect,
- toRectF,
- toRegion,
- toTransform,
- // Constants
- EMPTY_BUFFER,
- EMPTY_COLOR,
- EMPTY_RECT,
- EMPTY_RECTF,
- EMPTY_POINT,
- EMPTY_POINTF,
- EMPTY_MATRIX33,
- EMPTY_TRANSFORM,
-};
diff --git a/tools/winscope/src/trace/flickerlib/windows/Activity.ts b/tools/winscope/src/trace/flickerlib/windows/Activity.ts
deleted file mode 100644
index 1e3342f..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/Activity.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-import {Activity} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-Activity.fromProto = (proto: any, nextSeq: () => number): Activity => {
- if (proto == null) {
- return null;
- } else {
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.windowToken.windowContainer,
- /* protoChildren */ proto.windowToken.windowContainer?.children ?? [],
- /* isActivityInTree */ true,
- /* computedZ */ nextSeq,
- /* nameOverride */ proto.name,
- /* identifierOverride */ proto.identifier
- );
-
- const entry = new Activity(
- proto.state,
- proto.frontOfTask,
- proto.procId,
- proto.translucent,
- windowContainer
- );
-
- addAttributes(entry, proto);
- return entry;
- }
-};
-
-function addAttributes(entry: Activity, proto: any) {
- entry.proto = proto;
- entry.kind = entry.constructor.name;
- entry.shortName = shortenName(entry.name);
-}
-
-export {Activity};
diff --git a/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts b/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts
deleted file mode 100644
index 812b867..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/DisplayContent.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-import {DisplayContent, DisplayCutout, Rect, Rotation, toInsets, toRect} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-DisplayContent.fromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): DisplayContent => {
- if (proto == null) {
- return null;
- } else {
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.rootDisplayArea.windowContainer,
- /* protoChildren */ proto.rootDisplayArea.windowContainer?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq,
- /* nameOverride */ proto.displayInfo?.name ?? null
- );
- const displayRectWidth = proto.displayInfo?.logicalWidth ?? 0;
- const displayRectHeight = proto.displayInfo?.logicalHeight ?? 0;
- const appRectWidth = proto.displayInfo?.appWidth ?? 0;
- const appRectHeight = proto.displayInfo?.appHeight ?? 0;
- const defaultBounds = proto.pinnedStackController?.defaultBounds ?? null;
- const movementBounds = proto.pinnedStackController?.movementBounds ?? null;
-
- const entry = new DisplayContent(
- proto.id,
- proto.focusedRootTaskId,
- proto.resumedActivity?.title ?? '',
- proto.singleTaskInstance,
- toRect(defaultBounds),
- toRect(movementBounds),
- new Rect(0, 0, displayRectWidth, displayRectHeight),
- new Rect(0, 0, appRectWidth, appRectHeight),
- proto.dpi,
- proto.displayInfo?.flags ?? 0,
- toRect(proto.displayFrames?.stableBounds),
- proto.surfaceSize,
- proto.focusedApp,
- proto.appTransition?.lastUsedAppTransition ?? '',
- proto.appTransition?.appTransitionState ?? '',
- Rotation.Companion.getByValue(proto.displayRotation?.rotation ?? 0),
- proto.displayRotation?.lastOrientation ?? 0,
- createDisplayCutout(proto.displayInfo?.cutout),
- windowContainer
- );
-
- addAttributes(entry, proto);
- return entry;
- }
-};
-
-function createDisplayCutout(proto: any | null): DisplayCutout | null {
- if (proto == null) {
- return null;
- } else {
- return new DisplayCutout(
- toInsets(proto?.insets),
- toRect(proto?.boundLeft),
- toRect(proto?.boundTop),
- toRect(proto?.boundRight),
- toRect(proto?.boundBottom),
- toInsets(proto?.waterfallInsets)
- );
- }
-}
-
-function addAttributes(entry: DisplayContent, proto: any) {
- entry.proto = proto;
- entry.kind = entry.constructor.name;
- entry.shortName = shortenName(entry.name);
-}
-
-export {DisplayContent};
diff --git a/tools/winscope/src/trace/flickerlib/windows/Task.ts b/tools/winscope/src/trace/flickerlib/windows/Task.ts
deleted file mode 100644
index f5e4878..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/Task.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-import {Task, toRect} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-Task.fromProto = (proto: any, isActivityInTree: boolean, nextSeq: () => number): Task => {
- if (proto == null) {
- return null;
- } else {
- const windowContainerProto = proto.taskFragment?.windowContainer ?? proto.windowContainer;
- const windowContainer = WindowContainer.fromProto(
- /* proto */ windowContainerProto,
- /* protoChildren */ windowContainerProto?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq
- );
-
- const entry = new Task(
- proto.taskFragment?.activityType ?? proto.activityType,
- proto.fillsParent,
- toRect(proto.bounds),
- proto.id,
- proto.rootTaskId,
- proto.taskFragment?.displayId,
- toRect(proto.lastNonFullscreenBounds),
- proto.realActivity,
- proto.origActivity,
- proto.resizeMode,
- proto.resumedActivity?.title ?? '',
- proto.animatingBounds,
- proto.surfaceWidth,
- proto.surfaceHeight,
- proto.createdByOrganizer,
- proto.taskFragment?.minWidth ?? proto.minWidth,
- proto.taskFragment?.minHeight ?? proto.minHeight,
- windowContainer
- );
-
- addAttributes(entry, proto);
- return entry;
- }
-};
-
-function addAttributes(entry: Task, proto: any) {
- entry.proto = proto;
- entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
- entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
- entry.kind = entry.constructor.name;
- entry.shortName = shortenName(entry.name);
-}
-
-export {Task};
diff --git a/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts b/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts
deleted file mode 100644
index b6613af..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/TaskFragment.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2021, 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.
- */
-
-import {TaskFragment} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-TaskFragment.fromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): TaskFragment => {
- if (proto == null) {
- return null;
- } else {
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.windowContainer,
- /* protoChildren */ proto.windowContainer?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq
- );
- const entry = new TaskFragment(
- proto.activityType,
- proto.displayId,
- proto.minWidth,
- proto.minHeight,
- windowContainer
- );
-
- addAttributes(entry, proto);
- return entry;
- }
-};
-
-function addAttributes(entry: TaskFragment, proto: any) {
- entry.proto = proto;
- entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
- entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
- entry.kind = entry.constructor.name;
- entry.shortName = shortenName(entry.name);
-}
-
-export {TaskFragment};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts
deleted file mode 100644
index f18d09b..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/WindowContainer.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-import {shortenName} from '../mixin';
-
-import {
- Configuration,
- ConfigurationContainer,
- toRect,
- WindowConfiguration,
- WindowContainer,
-} from '../common';
-
-import {Activity} from './Activity';
-import {DisplayArea} from './DisplayArea';
-import {DisplayContent} from './DisplayContent';
-import {Task} from './Task';
-import {TaskFragment} from './TaskFragment';
-import {WindowState} from './WindowState';
-import {WindowToken} from './WindowToken';
-
-WindowContainer.fromProto = (
- proto: any,
- protoChildren: any[],
- isActivityInTree: boolean,
- nextSeq: () => number,
- nameOverride: string | null = null,
- identifierOverride: string | null = null,
- tokenOverride: any = null,
- visibleOverride: boolean | null = null
-): WindowContainer => {
- if (proto == null) {
- return null;
- }
-
- const containerOrder = nextSeq();
- const children = protoChildren
- .filter((it) => it != null)
- .map((it) => WindowContainer.childrenFromProto(it, isActivityInTree, nextSeq))
- .filter((it) => it != null);
-
- const identifier: any = identifierOverride ?? proto.identifier;
- const name: string = nameOverride ?? identifier?.title ?? '';
- const token: string = tokenOverride?.toString(16) ?? identifier?.hashCode?.toString(16) ?? '';
-
- const config = createConfigurationContainer(proto.configurationContainer);
- const entry = new WindowContainer(
- name,
- token,
- proto.orientation,
- proto.surfaceControl?.layerId ?? 0,
- visibleOverride ?? proto.visible,
- config,
- children,
- containerOrder
- );
-
- addAttributes(entry, proto);
- return entry;
-};
-
-function addAttributes(entry: WindowContainer, proto: any) {
- entry.proto = proto;
- entry.kind = entry.constructor.name;
- entry.shortName = shortenName(entry.name);
-}
-
-type WindowContainerChildType =
- | DisplayContent
- | DisplayArea
- | Task
- | TaskFragment
- | Activity
- | WindowToken
- | WindowState
- | WindowContainer;
-
-WindowContainer.childrenFromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): WindowContainerChildType => {
- return (
- DisplayContent.fromProto(proto.displayContent, isActivityInTree, nextSeq) ??
- DisplayArea.fromProto(proto.displayArea, isActivityInTree, nextSeq) ??
- Task.fromProto(proto.task, isActivityInTree, nextSeq) ??
- TaskFragment.fromProto(proto.taskFragment, isActivityInTree, nextSeq) ??
- Activity.fromProto(proto.activity, nextSeq) ??
- WindowToken.fromProto(proto.windowToken, isActivityInTree, nextSeq) ??
- WindowState.fromProto(proto.window, isActivityInTree, nextSeq) ??
- WindowContainer.fromProto(proto.windowContainer, nextSeq)
- );
-};
-
-function createConfigurationContainer(proto: any): ConfigurationContainer {
- const entry = ConfigurationContainer.Companion.from(
- createConfiguration(proto?.overrideConfiguration ?? null),
- createConfiguration(proto?.fullConfiguration ?? null),
- createConfiguration(proto?.mergedOverrideConfiguration ?? null)
- );
-
- entry.obj = entry;
- return entry;
-}
-
-function createConfiguration(proto: any): Configuration {
- if (proto == null) {
- return null;
- }
- let windowConfiguration = null;
-
- if (proto != null && proto.windowConfiguration != null) {
- windowConfiguration = createWindowConfiguration(proto.windowConfiguration);
- }
-
- return Configuration.Companion.from(
- windowConfiguration,
- proto?.densityDpi ?? 0,
- proto?.orientation ?? 0,
- proto?.screenHeightDp ?? 0,
- proto?.screenHeightDp ?? 0,
- proto?.smallestScreenWidthDp ?? 0,
- proto?.screenLayout ?? 0,
- proto?.uiMode ?? 0
- );
-}
-
-function createWindowConfiguration(proto: any): WindowConfiguration {
- return WindowConfiguration.Companion.from(
- toRect(proto.appBounds),
- toRect(proto.bounds),
- toRect(proto.maxBounds),
- proto.windowingMode,
- proto.activityType
- );
-}
-
-export {WindowContainer};
diff --git a/tools/winscope/src/trace/flickerlib/windows/WindowState.ts b/tools/winscope/src/trace/flickerlib/windows/WindowState.ts
deleted file mode 100644
index 3d47c4b..0000000
--- a/tools/winscope/src/trace/flickerlib/windows/WindowState.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright 2020, 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.
- */
-
-import {Size, toRect, WindowLayoutParams, WindowState} from '../common';
-import {shortenName} from '../mixin';
-import {WindowContainer} from './WindowContainer';
-
-WindowState.fromProto = (
- proto: any,
- isActivityInTree: boolean,
- nextSeq: () => number
-): WindowState => {
- if (proto == null) {
- return null;
- } else {
- const windowParams = createWindowLayoutParams(proto.attributes);
- const identifierName = getIdentifier(proto);
- const windowType = getWindowType(proto, identifierName);
- const name = getName(identifierName);
- const windowContainer = WindowContainer.fromProto(
- /* proto */ proto.windowContainer,
- /* protoChildren */ proto.windowContainer?.children ?? [],
- /* isActivityInTree */ isActivityInTree,
- /* computedZ */ nextSeq,
- /* nameOverride */ name,
- /* identifierOverride */ proto.identifier
- );
-
- const entry = new WindowState(
- windowParams,
- proto.displayId,
- proto.stackId,
- proto.animator?.surface?.layer ?? 0,
- proto.animator?.surface?.shown ?? false,
- windowType,
- new Size(proto.requestedWidth, proto.requestedHeight),
- toRect(proto.surfacePosition),
- toRect(proto.windowFrames?.frame ?? null),
- toRect(proto.windowFrames?.containingFrame ?? null),
- toRect(proto.windowFrames?.parentFrame ?? null),
- toRect(proto.windowFrames?.contentFrame ?? null),
- toRect(proto.windowFrames?.contentInsets ?? null),
- toRect(proto.surfaceInsets),
- toRect(proto.givenContentInsets),
- toRect(proto.animator?.lastClipRect ?? null),
- windowContainer,
- /* isAppWindow */ isActivityInTree
- );
-
- addAttributes(entry, proto);
- return entry;
- }
-};
-
-function createWindowLayoutParams(proto: any): WindowLayoutParams {
- return new WindowLayoutParams(
- /* type */ proto?.type ?? 0,
- /* x */ proto?.x ?? 0,
- /* y */ proto?.y ?? 0,
- /* width */ proto?.width ?? 0,
- /* height */ proto?.height ?? 0,
- /* horizontalMargin */ proto?.horizontalMargin ?? 0,
- /* verticalMargin */ proto?.verticalMargin ?? 0,
- /* gravity */ proto?.gravity ?? 0,
- /* softInputMode */ proto?.softInputMode ?? 0,
- /* format */ proto?.format ?? 0,
- /* windowAnimations */ proto?.windowAnimations ?? 0,
- /* alpha */ proto?.alpha ?? 0,
- /* screenBrightness */ proto?.screenBrightness ?? 0,
- /* buttonBrightness */ proto?.buttonBrightness ?? 0,
- /* rotationAnimation */ proto?.rotationAnimation ?? 0,
- /* preferredRefreshRate */ proto?.preferredRefreshRate ?? 0,
- /* preferredDisplayModeId */ proto?.preferredDisplayModeId ?? 0,
- /* hasSystemUiListeners */ proto?.hasSystemUiListeners ?? false,
- /* inputFeatureFlags */ proto?.inputFeatureFlags ?? 0,
- /* userActivityTimeout */ proto?.userActivityTimeout ?? 0,
- /* colorMode */ proto?.colorMode ?? 0,
- /* flags */ proto?.flags ?? 0,
- /* privateFlags */ proto?.privateFlags ?? 0,
- /* systemUiVisibilityFlags */ proto?.systemUiVisibilityFlags ?? 0,
- /* subtreeSystemUiVisibilityFlags */ proto?.subtreeSystemUiVisibilityFlags ?? 0,
- /* appearance */ proto?.appearance ?? 0,
- /* behavior */ proto?.behavior ?? 0,
- /* fitInsetsTypes */ proto?.fitInsetsTypes ?? 0,
- /* fitInsetsSides */ proto?.fitInsetsSides ?? 0,
- /* fitIgnoreVisibility */ proto?.fitIgnoreVisibility ?? false
- );
-}
-
-function getWindowType(proto: any, identifierName: string): number {
- if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
- return WindowState.WINDOW_TYPE_STARTING;
- } else if (proto.animatingExit) {
- return WindowState.WINDOW_TYPE_EXITING;
- } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
- return WindowState.WINDOW_TYPE_STARTING;
- }
-
- return 0;
-}
-
-function getName(identifierName: string): string {
- let name = identifierName;
-
- if (identifierName.startsWith(WindowState.STARTING_WINDOW_PREFIX)) {
- name = identifierName.substring(WindowState.STARTING_WINDOW_PREFIX.length);
- } else if (identifierName.startsWith(WindowState.DEBUGGER_WINDOW_PREFIX)) {
- name = identifierName.substring(WindowState.DEBUGGER_WINDOW_PREFIX.length);
- }
-
- return name;
-}
-
-function getIdentifier(proto: any): string {
- return proto.windowContainer.identifier?.title ?? proto.identifier?.title ?? '';
-}
-
-function addAttributes(entry: WindowState, proto: any) {
- entry.kind = entry.constructor.name;
- entry.rect = entry.frame;
- entry.rect.ref = entry;
- entry.rect.label = entry.name;
- entry.proto = proto;
- entry.proto.configurationContainer = proto.windowContainer?.configurationContainer;
- entry.proto.surfaceControl = proto.windowContainer?.surfaceControl;
- entry.shortName = shortenName(entry.name);
-}
-
-export {WindowState};
diff --git a/tools/winscope/src/trace/frame_mapper.ts b/tools/winscope/src/trace/frame_mapper.ts
index b4f4dd7..8589a8d 100644
--- a/tools/winscope/src/trace/frame_mapper.ts
+++ b/tools/winscope/src/trace/frame_mapper.ts
@@ -15,6 +15,7 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {CustomQueryType} from './custom_query';
import {FrameMapBuilder} from './frame_map_builder';
import {FramesRange, TraceEntry} from './trace';
import {Traces} from './traces';
@@ -100,19 +101,18 @@
}
const transactions = assertDefined(this.traces.getTrace(TraceType.TRANSACTIONS));
+ const transactionEntries = await transactions.customQuery(CustomQueryType.VSYNCID);
+
const surfaceFlinger = assertDefined(this.traces.getTrace(TraceType.SURFACE_FLINGER));
+ const surfaceFlingerEntries = await surfaceFlinger.customQuery(CustomQueryType.VSYNCID);
const vsyncIdToFrames = new Map<bigint, FramesRange>();
- for (let srcEntryIndex = 0; srcEntryIndex < surfaceFlinger.lengthEntries; ++srcEntryIndex) {
- const srcEntry = surfaceFlinger.getEntry(srcEntryIndex);
- const vsyncId = await this.getVsyncIdProperty(srcEntry, 'vSyncId');
- if (vsyncId === undefined) {
- continue;
- }
+ surfaceFlingerEntries.forEach((srcEntry) => {
+ const vsyncId = srcEntry.getValue();
const srcFrames = srcEntry.getFramesRange();
if (!srcFrames) {
- continue;
+ return;
}
let frames = vsyncIdToFrames.get(vsyncId);
if (!frames) {
@@ -121,20 +121,16 @@
frames.start = Math.min(frames.start, srcFrames.start);
frames.end = Math.max(frames.end, srcFrames.end);
vsyncIdToFrames.set(vsyncId, frames);
- }
+ });
- for (let dstEntryIndex = 0; dstEntryIndex < transactions.lengthEntries; ++dstEntryIndex) {
- const dstEntry = transactions.getEntry(dstEntryIndex);
- const vsyncId = await this.getVsyncIdProperty(dstEntry, 'vsyncId');
- if (vsyncId === undefined) {
- continue;
- }
+ transactionEntries.forEach((dstEntry) => {
+ const vsyncId = dstEntry.getValue();
const frames = vsyncIdToFrames.get(vsyncId);
if (frames === undefined) {
- continue;
+ return;
}
frameMapBuilder.setFrames(dstEntry.getIndex(), frames);
- }
+ });
const frameMap = frameMapBuilder.build();
transactions.setFrameInfo(frameMap, frameMap.getFullTraceFramesRange());
@@ -278,22 +274,4 @@
const lengthFrames = framesRange ? framesRange.end : 0;
return new FrameMapBuilder(dstTrace.lengthEntries, lengthFrames);
}
-
- private async getVsyncIdProperty(
- entry: TraceEntry<object>,
- propertyKey: string
- ): Promise<bigint | undefined> {
- const entryValue = await entry.getValue();
- const vsyncId = (entryValue as any)[propertyKey];
- if (vsyncId === undefined) {
- console.error(`Failed to get trace entry's '${propertyKey}' property:`, entryValue);
- return undefined;
- }
- try {
- return BigInt(vsyncId.toString());
- } catch (e) {
- console.error(`Failed to convert trace entry's vsyncId to bigint:`, entryValue);
- return undefined;
- }
- }
}
diff --git a/tools/winscope/src/trace/frame_mapper_test.ts b/tools/winscope/src/trace/frame_mapper_test.ts
index 5a3619d..b359306 100644
--- a/tools/winscope/src/trace/frame_mapper_test.ts
+++ b/tools/winscope/src/trace/frame_mapper_test.ts
@@ -14,15 +14,16 @@
* limitations under the License.
*/
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
import {TracesUtils} from 'test/unit/traces_utils';
import {TraceBuilder} from 'test/unit/trace_builder';
-import {LayerTraceEntry} from './flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from './flickerlib/windows/WindowManagerState';
+import {RealTimestamp} from '../common/time';
+import {CustomQueryType} from './custom_query';
import {FrameMapper} from './frame_mapper';
import {AbsoluteFrameIndex} from './index_types';
import {LogMessage} from './protolog';
import {ScreenRecordingTraceEntry} from './screen_recording';
-import {RealTimestamp} from './timestamp';
import {Trace} from './trace';
import {Traces} from './traces';
import {TraceType} from './trace_type';
@@ -271,22 +272,24 @@
// SURFACE_FLINGER: 0 1 2
transactions = new TraceBuilder<object>()
.setEntries([
- {id: 0, vsyncId: createVsyncId(0)},
- {id: 1, vsyncId: createVsyncId(10)},
- {id: 2, vsyncId: createVsyncId(10)},
- {id: 3, vsyncId: createVsyncId(20)},
- {id: 4, vsyncId: createVsyncId(30)},
+ 'entry-0' as unknown as LayerTraceEntry,
+ 'entry-1' as unknown as LayerTraceEntry,
+ 'entry-2' as unknown as LayerTraceEntry,
+ 'entry-3' as unknown as LayerTraceEntry,
+ 'entry-4' as unknown as LayerTraceEntry,
])
.setTimestamps([time0, time1, time2, time5, time6])
+ .setParserCustomQueryResult(CustomQueryType.VSYNCID, [0n, 10n, 10n, 20n, 30n])
.build();
surfaceFlinger = new TraceBuilder<LayerTraceEntry>()
.setEntries([
- {id: 0, vSyncId: createVsyncId(0)} as unknown as LayerTraceEntry,
- {id: 1, vSyncId: createVsyncId(10)} as unknown as LayerTraceEntry,
- {id: 2, vSyncId: createVsyncId(20)} as unknown as LayerTraceEntry,
+ 'entry-0' as unknown as object,
+ 'entry-1' as unknown as object,
+ 'entry-2' as unknown as object,
])
.setTimestamps([time0, time1, time2])
+ .setParserCustomQueryResult(CustomQueryType.VSYNCID, [0n, 10n, 20n])
.build();
traces = new Traces();
@@ -416,12 +419,4 @@
expect(await TracesUtils.extractFrames(traces)).toEqual(expectedFrames);
});
});
-
- const createVsyncId = (value: number): object => {
- return {
- toString() {
- return value.toString();
- },
- };
- };
});
diff --git a/tools/winscope/src/trace/parser.ts b/tools/winscope/src/trace/parser.ts
index 302baf0..6a99186 100644
--- a/tools/winscope/src/trace/parser.ts
+++ b/tools/winscope/src/trace/parser.ts
@@ -14,14 +14,24 @@
* limitations under the License.
*/
-import {Timestamp, TimestampType} from './timestamp';
+import {Timestamp, TimestampType} from 'common/time';
+import {
+ CustomQueryParamTypeMap,
+ CustomQueryParserResultTypeMap,
+ CustomQueryType,
+} from './custom_query';
+import {AbsoluteEntryIndex, EntriesRange} from './index_types';
import {TraceType} from './trace_type';
export interface Parser<T> {
getTraceType(): TraceType;
getLengthEntries(): number;
getTimestamps(type: TimestampType): Timestamp[] | undefined;
- getEntry(index: number, timestampType: TimestampType): Promise<T>;
-
+ getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange,
+ param?: CustomQueryParamTypeMap[Q]
+ ): Promise<CustomQueryParserResultTypeMap[Q]>;
getDescriptors(): string[];
}
diff --git a/tools/winscope/src/trace/parser_mock.ts b/tools/winscope/src/trace/parser_mock.ts
index 77adbf3..0af2895 100644
--- a/tools/winscope/src/trace/parser_mock.ts
+++ b/tools/winscope/src/trace/parser_mock.ts
@@ -14,12 +14,18 @@
* limitations under the License.
*/
+import {RealTimestamp, Timestamp, TimestampType} from '../common/time';
+import {CustomQueryParserResultTypeMap, CustomQueryType} from './custom_query';
+import {AbsoluteEntryIndex, EntriesRange} from './index_types';
import {Parser} from './parser';
-import {RealTimestamp, Timestamp, TimestampType} from './timestamp';
import {TraceType} from './trace_type';
export class ParserMock<T> implements Parser<T> {
- constructor(private readonly timestamps: RealTimestamp[], private readonly entries: T[]) {
+ constructor(
+ private readonly timestamps: RealTimestamp[],
+ private readonly entries: T[],
+ private readonly customQueryResult: Map<CustomQueryType, object>
+ ) {
if (timestamps.length !== entries.length) {
throw new Error(`Timestamps and entries must have the same length`);
}
@@ -40,10 +46,26 @@
return this.timestamps;
}
- getEntry(index: number): Promise<T> {
+ getEntry(index: AbsoluteEntryIndex): Promise<T> {
return Promise.resolve(this.entries[index]);
}
+ customQuery<Q extends CustomQueryType>(
+ type: Q,
+ entriesRange: EntriesRange
+ ): Promise<CustomQueryParserResultTypeMap[Q]> {
+ let result = this.customQueryResult.get(type);
+ if (result === undefined) {
+ throw new Error(
+ `This mock was not configured to support custom query type '${type}'. Something missing in your test set up?`
+ );
+ }
+ if (Array.isArray(result)) {
+ result = result.slice(entriesRange.start, entriesRange.end);
+ }
+ return Promise.resolve(result) as Promise<CustomQueryParserResultTypeMap[Q]>;
+ }
+
getDescriptors(): string[] {
return ['MockTrace'];
}
diff --git a/tools/winscope/src/trace/protolog.ts b/tools/winscope/src/trace/protolog.ts
index 7139a08..a20b5aa 100644
--- a/tools/winscope/src/trace/protolog.ts
+++ b/tools/winscope/src/trace/protolog.ts
@@ -16,7 +16,7 @@
import {TimeUtils} from 'common/time_utils';
import configJson from '../../../../../frameworks/base/data/etc/services.core.protolog.json';
-import {ElapsedTimestamp, RealTimestamp, TimestampType} from './timestamp';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from '../common/time';
class LogMessage {
text: string;
diff --git a/tools/winscope/src/trace/screen_recording_utils.ts b/tools/winscope/src/trace/screen_recording_utils.ts
index e882f8c..38cb5d6 100644
--- a/tools/winscope/src/trace/screen_recording_utils.ts
+++ b/tools/winscope/src/trace/screen_recording_utils.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {Timestamp} from './timestamp';
+import {Timestamp} from '../common/time';
class ScreenRecordingUtils {
// Video time correction epsilon. Without correction, we could display the previous frame.
diff --git a/tools/winscope/src/trace/timestamp_test.ts b/tools/winscope/src/trace/timestamp_test.ts
deleted file mode 100644
index 91a8b1a..0000000
--- a/tools/winscope/src/trace/timestamp_test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.
- */
-
-import {Timestamp, TimestampType} from './timestamp';
-
-describe('Timestamp', () => {
- describe('from', () => {
- it('throws when missing elapsed timestamp', () => {
- expect(() => {
- Timestamp.from(TimestampType.REAL, 100n);
- }).toThrow();
- });
-
- it('can create real timestamp', () => {
- const timestamp = Timestamp.from(TimestampType.REAL, 100n, 500n);
- expect(timestamp.getType()).toBe(TimestampType.REAL);
- expect(timestamp.getValueNs()).toBe(600n);
- });
-
- it('can create elapsed timestamp', () => {
- let timestamp = Timestamp.from(TimestampType.ELAPSED, 100n, 500n);
- expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
- expect(timestamp.getValueNs()).toBe(100n);
-
- timestamp = Timestamp.from(TimestampType.ELAPSED, 100n);
- expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
- expect(timestamp.getValueNs()).toBe(100n);
- });
- });
-});
diff --git a/tools/winscope/src/trace/trace.ts b/tools/winscope/src/trace/trace.ts
index c2be329..ea6a69d 100644
--- a/tools/winscope/src/trace/trace.ts
+++ b/tools/winscope/src/trace/trace.ts
@@ -15,6 +15,14 @@
*/
import {ArrayUtils} from 'common/array_utils';
+import {Timestamp, TimestampType} from '../common/time';
+import {
+ CustomQueryParamTypeMap,
+ CustomQueryParserResultTypeMap,
+ CustomQueryResultTypeMap,
+ CustomQueryType,
+ ProcessParserResult,
+} from './custom_query';
import {FrameMap} from './frame_map';
import {
AbsoluteEntryIndex,
@@ -24,7 +32,6 @@
RelativeEntryIndex,
} from './index_types';
import {Parser} from './parser';
-import {Timestamp, TimestampType} from './timestamp';
import {TraceType} from './trace_type';
export {
@@ -35,13 +42,13 @@
RelativeEntryIndex,
} from './index_types';
-export class TraceEntry<T> {
+export abstract class TraceEntry<T> {
constructor(
- private readonly fullTrace: Trace<T>,
- private readonly parser: Parser<T>,
- private readonly index: AbsoluteEntryIndex,
- private readonly timestamp: Timestamp,
- private readonly framesRange: FramesRange | undefined
+ protected readonly fullTrace: Trace<T>,
+ protected readonly parser: Parser<T>,
+ protected readonly index: AbsoluteEntryIndex,
+ protected readonly timestamp: Timestamp,
+ protected readonly framesRange: FramesRange | undefined
) {}
getFullTrace(): Trace<T> {
@@ -65,11 +72,45 @@
return this.framesRange;
}
- async getValue(): Promise<T> {
+ abstract getValue(): any;
+}
+
+export class TraceEntryLazy<T> extends TraceEntry<T> {
+ constructor(
+ fullTrace: Trace<T>,
+ parser: Parser<T>,
+ index: AbsoluteEntryIndex,
+ timestamp: Timestamp,
+ framesRange: FramesRange | undefined
+ ) {
+ super(fullTrace, parser, index, timestamp, framesRange);
+ }
+
+ override async getValue(): Promise<T> {
return await this.parser.getEntry(this.index, this.timestamp.getType());
}
}
+export class TraceEntryEager<T, U> extends TraceEntry<T> {
+ private readonly value: U;
+
+ constructor(
+ fullTrace: Trace<T>,
+ parser: Parser<T>,
+ index: AbsoluteEntryIndex,
+ timestamp: Timestamp,
+ framesRange: FramesRange | undefined,
+ value: U
+ ) {
+ super(fullTrace, parser, index, timestamp, framesRange);
+ this.value = value;
+ }
+
+ override getValue(): U {
+ return this.value;
+ }
+}
+
export class Trace<T> {
readonly type: TraceType;
readonly lengthEntries: number;
@@ -77,42 +118,28 @@
private readonly parser: Parser<T>;
private readonly descriptors: string[];
private readonly fullTrace: Trace<T>;
- private timestampType: TimestampType | undefined;
+ private timestampType: TimestampType;
private readonly entriesRange: EntriesRange;
private frameMap?: FrameMap;
private framesRange?: FramesRange;
- static newUninitializedTrace<T>(parser: Parser<T>): Trace<T> {
+ static fromParser<T>(parser: Parser<T>, timestampType: TimestampType): Trace<T> {
return new Trace(
parser.getTraceType(),
parser,
parser.getDescriptors(),
undefined,
- undefined,
+ timestampType,
undefined
);
}
- static newInitializedTrace<T>(
- type: TraceType,
- entryProvider: Parser<T>,
- descriptors: string[],
- timestampType: TimestampType,
- entriesRange: EntriesRange
- ): Trace<T> {
- return new Trace(type, entryProvider, descriptors, undefined, timestampType, entriesRange);
- }
-
- init(timestampType: TimestampType) {
- this.timestampType = timestampType;
- }
-
- private constructor(
+ constructor(
type: TraceType,
parser: Parser<T>,
descriptors: string[],
fullTrace: Trace<T> | undefined,
- timestampType: TimestampType | undefined,
+ timestampType: TimestampType,
entriesRange: EntriesRange | undefined
) {
this.type = type;
@@ -147,23 +174,41 @@
return this.frameMap !== undefined;
}
- getEntry(index: RelativeEntryIndex): TraceEntry<T> {
- const entry = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
- if (entry < this.entriesRange.start || entry >= this.entriesRange.end) {
- throw new Error(
- `Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
- );
- }
- const frames = this.clampFramesRangeToSliceBounds(
- this.frameMap?.getFramesRange({start: entry, end: entry + 1})
- );
- return new TraceEntry<T>(
- this.fullTrace,
- this.parser,
- entry,
- this.getFullTraceTimestamps()[entry],
- frames
- );
+ getEntry(index: RelativeEntryIndex): TraceEntryLazy<T> {
+ return this.getEntryInternal(index, (index, timestamp, frames) => {
+ return new TraceEntryLazy<T>(this.fullTrace, this.parser, index, timestamp, frames);
+ });
+ }
+
+ async customQuery<Q extends CustomQueryType>(
+ type: Q,
+ param?: CustomQueryParamTypeMap[Q]
+ ): Promise<CustomQueryResultTypeMap<T>[Q]> {
+ const makeTraceEntry = <U>(index: RelativeEntryIndex, value: U): TraceEntryEager<T, U> => {
+ return this.getEntryInternal(index, (index, timestamp, frames) => {
+ return new TraceEntryEager<T, U>(
+ this.fullTrace,
+ this.parser,
+ index,
+ timestamp,
+ frames,
+ value
+ );
+ });
+ };
+
+ const processParserResult = ProcessParserResult[type] as (
+ parserResult: CustomQueryParserResultTypeMap[Q],
+ make: typeof makeTraceEntry
+ ) => CustomQueryResultTypeMap<T>[Q];
+
+ const parserResult = (await this.parser.customQuery(
+ type,
+ this.entriesRange,
+ param
+ )) as CustomQueryParserResultTypeMap[Q];
+ const finalResult = processParserResult(parserResult, makeTraceEntry);
+ return Promise.resolve(finalResult);
}
getFrame(frame: AbsoluteFrameIndex): Trace<T> {
@@ -172,7 +217,7 @@
return this.createSlice(entries, {start: frame, end: frame + 1});
}
- findClosestEntry(time: Timestamp): TraceEntry<T> | undefined {
+ findClosestEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
@@ -201,7 +246,7 @@
return this.getEntry(entry - this.entriesRange.start);
}
- findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntry<T> | undefined {
+ findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
@@ -222,7 +267,7 @@
return entry;
}
- findFirstGreaterEntry(time: Timestamp): TraceEntry<T> | undefined {
+ findFirstGreaterEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
@@ -243,7 +288,7 @@
return entry;
}
- findLastLowerOrEqualEntry(timestamp: Timestamp): TraceEntry<T> | undefined {
+ findLastLowerOrEqualEntry(timestamp: Timestamp): TraceEntryLazy<T> | undefined {
if (this.lengthEntries === 0) {
return undefined;
}
@@ -257,7 +302,7 @@
return this.getEntry(firstGreater.getIndex() - this.entriesRange.start - 1);
}
- findLastLowerEntry(timestamp: Timestamp): TraceEntry<T> | undefined {
+ findLastLowerEntry(timestamp: Timestamp): TraceEntryLazy<T> | undefined {
if (this.lengthEntries === 0) {
return undefined;
}
@@ -321,13 +366,13 @@
return this.createSlice(entries, frames);
}
- forEachEntry(callback: (pos: TraceEntry<T>, index: RelativeEntryIndex) => void) {
+ forEachEntry(callback: (pos: TraceEntryLazy<T>, index: RelativeEntryIndex) => void) {
for (let index = 0; index < this.lengthEntries; ++index) {
callback(this.getEntry(index), index);
}
}
- mapEntry<U>(callback: (entry: TraceEntry<T>, index: RelativeEntryIndex) => U): U[] {
+ mapEntry<U>(callback: (entry: TraceEntryLazy<T>, index: RelativeEntryIndex) => U): U[] {
const result: U[] = [];
this.forEachEntry((entry, index) => {
result.push(callback(entry, index));
@@ -365,6 +410,27 @@
return this.framesRange;
}
+ private getEntryInternal<EntryType extends TraceEntryLazy<T> | TraceEntryEager<T, any>>(
+ index: RelativeEntryIndex,
+ makeEntry: (
+ absoluteIndex: AbsoluteEntryIndex,
+ timestamp: Timestamp,
+ frames: FramesRange | undefined
+ ) => EntryType
+ ): EntryType {
+ const absoluteIndex = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
+ if (absoluteIndex < this.entriesRange.start || absoluteIndex >= this.entriesRange.end) {
+ throw new Error(
+ `Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
+ );
+ }
+ const timestamp = this.getFullTraceTimestamps()[absoluteIndex];
+ const frames = this.clampFramesRangeToSliceBounds(
+ this.frameMap?.getFramesRange({start: absoluteIndex, end: absoluteIndex + 1})
+ );
+ return makeEntry(absoluteIndex, timestamp, frames);
+ }
+
private getFullTraceTimestamps(): Timestamp[] {
if (this.timestampType === undefined) {
throw new Error('Forgot to initialize trace?');
diff --git a/tools/winscope/src/trace/trace_data_utils.ts b/tools/winscope/src/trace/trace_data_utils.ts
new file mode 100644
index 0000000..a6aaff5
--- /dev/null
+++ b/tools/winscope/src/trace/trace_data_utils.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {Rect} from 'common/geometry_utils';
+import {Transform} from 'parsers/transform_utils';
+
+/* DATA INTERFACES */
+class Item {
+ constructor(public id: string, public label: string) {}
+}
+
+class TreeNode<T> extends Item {
+ constructor(id: string, label: string, public children: Array<TreeNode<T> & T> = []) {
+ super(id, label);
+ }
+}
+
+class TraceRect extends Item implements Rect {
+ constructor(
+ id: string,
+ label: string,
+ public x: number,
+ public y: number,
+ public w: number,
+ public h: number,
+ public cornerRadius: number,
+ public transform: Transform,
+ public zAbs: number,
+ public groupId: number,
+ public isVisible: boolean,
+ public isDisplay: boolean,
+ public isVirtual: boolean
+ ) {
+ super(id, label);
+ }
+}
+
+type Proto = any;
+
+/* GET PROPERTIES TYPES */
+type GetPropertiesFromProtoType = (proto: Proto) => PropertyTreeNode;
+type GetPropertiesType = () => PropertyTreeNode;
+
+/* MIXINS */
+interface PropertiesGetter {
+ getProperties: GetPropertiesType;
+}
+
+enum PropertySource {
+ PROTO,
+ DEFAULT,
+ CALCULATED,
+}
+
+interface PropertyDetails {
+ value: string;
+ source: PropertySource;
+}
+
+interface AssociatedProperty {
+ property: null | PropertyDetails;
+}
+
+interface Constructor<T = {}> {
+ new (...args: any[]): T;
+}
+
+type PropertyTreeNode = TreeNode<AssociatedProperty> & AssociatedProperty;
+type HierarchyTreeNode = TreeNode<PropertiesGetter> & PropertiesGetter;
+
+const EMPTY_OBJ_STRING = '{empty}';
+const EMPTY_ARRAY_STRING = '[empty]';
+
+export {
+ Item,
+ TreeNode,
+ TraceRect,
+ Proto,
+ GetPropertiesType,
+ GetPropertiesFromProtoType,
+ PropertiesGetter,
+ PropertySource,
+ PropertyDetails,
+ AssociatedProperty,
+ Constructor,
+ PropertyTreeNode,
+ HierarchyTreeNode,
+ EMPTY_OBJ_STRING,
+ EMPTY_ARRAY_STRING,
+};
diff --git a/tools/winscope/src/trace/trace_entry_finder.ts b/tools/winscope/src/trace/trace_entry_finder.ts
index 33c58d5..94341b8 100644
--- a/tools/winscope/src/trace/trace_entry_finder.ts
+++ b/tools/winscope/src/trace/trace_entry_finder.ts
@@ -16,20 +16,9 @@
import {Trace, TraceEntry} from './trace';
import {TracePosition} from './trace_position';
-import {TraceType} from './trace_type';
+import {TraceTypeUtils} from './trace_type';
export class TraceEntryFinder {
- static readonly UI_PIPELINE_ORDER = [
- TraceType.INPUT_METHOD_CLIENTS,
- TraceType.INPUT_METHOD_SERVICE,
- TraceType.INPUT_METHOD_MANAGER_SERVICE,
- TraceType.PROTO_LOG,
- TraceType.WINDOW_MANAGER,
- TraceType.TRANSACTIONS,
- TraceType.SURFACE_FLINGER,
- TraceType.SCREEN_RECORDING,
- ];
-
static findCorrespondingEntry<T>(
trace: Trace<T>,
position: TracePosition
@@ -47,20 +36,13 @@
if (position.entry) {
const entryTraceType = position.entry.getFullTrace().type;
-
- const indexPosition = TraceEntryFinder.UI_PIPELINE_ORDER.findIndex((type) => {
- return type === entryTraceType;
- });
- const indexTrace = TraceEntryFinder.UI_PIPELINE_ORDER.findIndex((type) => {
- return type === trace.type;
- });
-
- if (indexPosition !== undefined && indexTrace !== undefined) {
- if (indexPosition < indexTrace) {
- return trace.findFirstGreaterEntry(position.entry.getTimestamp());
- } else {
- return trace.findLastLowerEntry(position.entry.getTimestamp());
- }
+ const timestamp = position.entry.getTimestamp();
+ if (TraceTypeUtils.compareByUiPipelineOrder(entryTraceType, trace.type)) {
+ return (
+ trace.findFirstGreaterEntry(timestamp) ?? trace.findFirstGreaterOrEqualEntry(timestamp)
+ );
+ } else {
+ return trace.findLastLowerEntry(timestamp) ?? trace.findLastLowerOrEqualEntry(timestamp);
}
}
diff --git a/tools/winscope/src/trace/trace_entry_test.ts b/tools/winscope/src/trace/trace_entry_test.ts
index 7859da6..9b4de44 100644
--- a/tools/winscope/src/trace/trace_entry_test.ts
+++ b/tools/winscope/src/trace/trace_entry_test.ts
@@ -15,7 +15,7 @@
*/
import {TraceBuilder} from 'test/unit/trace_builder';
-import {RealTimestamp} from './timestamp';
+import {RealTimestamp} from '../common/time';
import {Trace} from './trace';
describe('TraceEntry', () => {
diff --git a/tools/winscope/src/trace/trace_position.ts b/tools/winscope/src/trace/trace_position.ts
index f0cd118..9f8889b 100644
--- a/tools/winscope/src/trace/trace_position.ts
+++ b/tools/winscope/src/trace/trace_position.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {Timestamp} from '../common/time';
import {AbsoluteFrameIndex} from './index_types';
-import {Timestamp} from './timestamp';
import {TraceEntry} from './trace';
export class TracePosition {
@@ -23,13 +23,14 @@
return new TracePosition(timestamp);
}
- static fromTraceEntry(entry: TraceEntry<{}>): TracePosition {
+ static fromTraceEntry(entry: TraceEntry<{}>, explicitTimestamp?: Timestamp): TracePosition {
let frame: AbsoluteFrameIndex | undefined;
if (entry.getFullTrace().hasFrameInfo()) {
const frames = entry.getFramesRange();
frame = frames && frames.start < frames.end ? frames.start : undefined;
}
- return new TracePosition(entry.getTimestamp(), frame, entry);
+ const timestamp = explicitTimestamp ? explicitTimestamp : entry.getTimestamp();
+ return new TracePosition(timestamp, frame, entry);
}
isEqual(other: TracePosition): boolean {
diff --git a/tools/winscope/src/trace/trace_test.ts b/tools/winscope/src/trace/trace_test.ts
index 3380c8e..3461c1a 100644
--- a/tools/winscope/src/trace/trace_test.ts
+++ b/tools/winscope/src/trace/trace_test.ts
@@ -16,9 +16,9 @@
import {TraceBuilder} from 'test/unit/trace_builder';
import {TraceUtils} from 'test/unit/trace_utils';
+import {RealTimestamp} from '../common/time';
import {FrameMapBuilder} from './frame_map_builder';
import {AbsoluteFrameIndex} from './index_types';
-import {RealTimestamp} from './timestamp';
import {Trace} from './trace';
describe('Trace', () => {
diff --git a/tools/winscope/src/trace/trace_type.ts b/tools/winscope/src/trace/trace_type.ts
index ee18a44..fff5de5 100644
--- a/tools/winscope/src/trace/trace_type.ts
+++ b/tools/winscope/src/trace/trace_type.ts
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Cuj, Event, Transition} from 'trace/flickerlib/common';
-import {LayerTraceEntry} from './flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from './flickerlib/windows/WindowManagerState';
+import {Cuj, Event, Transition} from 'flickerlib/common';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {HierarchyTreeNode, TraceRect, TreeNode} from 'trace/trace_data_utils';
import {LogMessage} from './protolog';
import {ScreenRecordingTraceEntry} from './screen_recording';
export enum TraceType {
- ACCESSIBILITY,
WINDOW_MANAGER,
SURFACE_FLINGER,
SCREEN_RECORDING,
@@ -30,7 +30,6 @@
WAYLAND_DUMP,
PROTO_LOG,
SYSTEM_UI,
- LAUNCHER,
INPUT_METHOD_CLIENTS,
INPUT_METHOD_MANAGER_SERVICE,
INPUT_METHOD_SERVICE,
@@ -44,31 +43,125 @@
TEST_TRACE_STRING,
TEST_TRACE_NUMBER,
VIEW_CAPTURE,
+ VIEW_CAPTURE_LAUNCHER_ACTIVITY,
+ VIEW_CAPTURE_TASKBAR_DRAG_LAYER,
+ VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
+}
+
+// view capture types
+export type ViewNode = any;
+export type FrameData = any;
+export type WindowData = any;
+
+export interface TreeAndRects {
+ tree: HierarchyTreeNode;
+ rects: TraceRect[];
}
export interface TraceEntryTypeMap {
- [TraceType.ACCESSIBILITY]: object;
- [TraceType.LAUNCHER]: object;
- [TraceType.PROTO_LOG]: LogMessage;
- [TraceType.SURFACE_FLINGER]: LayerTraceEntry;
- [TraceType.SCREEN_RECORDING]: ScreenRecordingTraceEntry;
- [TraceType.SYSTEM_UI]: object;
- [TraceType.TRANSACTIONS]: object;
- [TraceType.TRANSACTIONS_LEGACY]: object;
- [TraceType.WAYLAND]: object;
- [TraceType.WAYLAND_DUMP]: object;
- [TraceType.WINDOW_MANAGER]: WindowManagerState;
- [TraceType.INPUT_METHOD_CLIENTS]: object;
- [TraceType.INPUT_METHOD_MANAGER_SERVICE]: object;
- [TraceType.INPUT_METHOD_SERVICE]: object;
- [TraceType.EVENT_LOG]: Event;
- [TraceType.WM_TRANSITION]: object;
- [TraceType.SHELL_TRANSITION]: object;
- [TraceType.TRANSITION]: Transition;
- [TraceType.CUJS]: Cuj;
- [TraceType.TAG]: object;
- [TraceType.ERROR]: object;
- [TraceType.TEST_TRACE_STRING]: string;
- [TraceType.TEST_TRACE_NUMBER]: number;
- [TraceType.VIEW_CAPTURE]: object;
+ [TraceType.PROTO_LOG]: {new: LogMessage; legacy: LogMessage};
+ [TraceType.SURFACE_FLINGER]: {new: TreeAndRects; legacy: LayerTraceEntry};
+ [TraceType.SCREEN_RECORDING]: {new: ScreenRecordingTraceEntry; legacy: ScreenRecordingTraceEntry};
+ [TraceType.SYSTEM_UI]: {new: object; legacy: object};
+ [TraceType.TRANSACTIONS]: {new: TreeNode<any>; legacy: object};
+ [TraceType.TRANSACTIONS_LEGACY]: {new: TreeNode<any>; legacy: object};
+ [TraceType.WAYLAND]: {new: object; legacy: object};
+ [TraceType.WAYLAND_DUMP]: {new: object; legacy: object};
+ [TraceType.WINDOW_MANAGER]: {new: TreeAndRects; legacy: WindowManagerState};
+ [TraceType.INPUT_METHOD_CLIENTS]: {new: TreeNode<any>; legacy: object};
+ [TraceType.INPUT_METHOD_MANAGER_SERVICE]: {new: TreeNode<any>; legacy: object};
+ [TraceType.INPUT_METHOD_SERVICE]: {new: TreeNode<any>; legacy: object};
+ [TraceType.EVENT_LOG]: {new: TreeNode<any>; legacy: Event};
+ [TraceType.WM_TRANSITION]: {new: TreeNode<any>; legacy: object};
+ [TraceType.SHELL_TRANSITION]: {new: TreeNode<any>; legacy: object};
+ [TraceType.TRANSITION]: {new: TreeNode<any>; legacy: Transition};
+ [TraceType.CUJS]: {new: TreeNode<any>; legacy: Cuj};
+ [TraceType.TAG]: {new: object; legacy: object};
+ [TraceType.ERROR]: {new: object; legacy: object};
+ [TraceType.TEST_TRACE_STRING]: {new: string; legacy: string};
+ [TraceType.TEST_TRACE_NUMBER]: {new: number; legacy: number};
+ [TraceType.VIEW_CAPTURE]: {new: TreeNode<any>; legacy: object};
+ [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY]: {
+ new: TreeAndRects;
+ legacy: FrameData;
+ };
+ [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER]: {
+ new: TreeAndRects;
+ legacy: FrameData;
+ };
+ [TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER]: {
+ new: TreeAndRects;
+ legacy: FrameData;
+ };
+}
+
+export class TraceTypeUtils {
+ private static UI_PIPELINE_ORDER = [
+ TraceType.INPUT_METHOD_CLIENTS,
+ TraceType.INPUT_METHOD_SERVICE,
+ TraceType.INPUT_METHOD_MANAGER_SERVICE,
+ TraceType.PROTO_LOG,
+ TraceType.WINDOW_MANAGER,
+ TraceType.TRANSACTIONS,
+ TraceType.SURFACE_FLINGER,
+ TraceType.SCREEN_RECORDING,
+ ];
+
+ private static DISPLAY_ORDER = [
+ TraceType.SCREEN_RECORDING,
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.INPUT_METHOD_CLIENTS,
+ TraceType.INPUT_METHOD_MANAGER_SERVICE,
+ TraceType.INPUT_METHOD_SERVICE,
+ TraceType.TRANSACTIONS,
+ TraceType.TRANSACTIONS_LEGACY,
+ TraceType.PROTO_LOG,
+ TraceType.WM_TRANSITION,
+ TraceType.SHELL_TRANSITION,
+ TraceType.TRANSITION,
+ TraceType.VIEW_CAPTURE,
+ TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY,
+ TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER,
+ TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
+ ];
+
+ private static TRACES_WITH_VIEWERS = [
+ TraceType.SCREEN_RECORDING,
+ TraceType.SURFACE_FLINGER,
+ TraceType.WINDOW_MANAGER,
+ TraceType.INPUT_METHOD_CLIENTS,
+ TraceType.INPUT_METHOD_MANAGER_SERVICE,
+ TraceType.INPUT_METHOD_SERVICE,
+ TraceType.TRANSACTIONS,
+ TraceType.TRANSACTIONS_LEGACY,
+ TraceType.PROTO_LOG,
+ TraceType.TRANSITION,
+ TraceType.VIEW_CAPTURE,
+ TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY,
+ TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER,
+ TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
+ ];
+
+ static isTraceTypeWithViewer(t: TraceType): boolean {
+ return TraceTypeUtils.TRACES_WITH_VIEWERS.includes(t);
+ }
+
+ static compareByUiPipelineOrder(t: TraceType, u: TraceType) {
+ const tIndex = TraceTypeUtils.findIndexInOrder(t, TraceTypeUtils.UI_PIPELINE_ORDER);
+ const uIndex = TraceTypeUtils.findIndexInOrder(u, TraceTypeUtils.UI_PIPELINE_ORDER);
+ return tIndex >= 0 && uIndex >= 0 && tIndex < uIndex;
+ }
+
+ static compareByDisplayOrder(t: TraceType, u: TraceType) {
+ const tIndex = TraceTypeUtils.findIndexInOrder(t, TraceTypeUtils.DISPLAY_ORDER);
+ const uIndex = TraceTypeUtils.findIndexInOrder(u, TraceTypeUtils.DISPLAY_ORDER);
+ return tIndex - uIndex;
+ }
+
+ private static findIndexInOrder(traceType: TraceType, order: TraceType[]): number {
+ return order.findIndex((type) => {
+ return type === traceType;
+ });
+ }
}
diff --git a/tools/winscope/src/trace/traces.ts b/tools/winscope/src/trace/traces.ts
index 2c18572..20f2cdd 100644
--- a/tools/winscope/src/trace/traces.ts
+++ b/tools/winscope/src/trace/traces.ts
@@ -14,20 +14,20 @@
* limitations under the License.
*/
+import {Timestamp} from '../common/time';
import {AbsoluteFrameIndex} from './index_types';
-import {Timestamp} from './timestamp';
import {Trace} from './trace';
import {TraceEntryTypeMap, TraceType} from './trace_type';
export class Traces {
private traces = new Map<TraceType, Trace<{}>>();
- setTrace<T extends TraceType>(type: T, trace: Trace<TraceEntryTypeMap[T]>) {
+ setTrace<T extends TraceType>(type: T, trace: Trace<TraceEntryTypeMap[T]['legacy']>) {
this.traces.set(type, trace);
}
- getTrace<T extends TraceType>(type: T): Trace<TraceEntryTypeMap[T]> | undefined {
- return this.traces.get(type) as Trace<TraceEntryTypeMap[T]> | undefined;
+ getTrace<T extends TraceType>(type: T): Trace<TraceEntryTypeMap[T]['legacy']> | undefined {
+ return this.traces.get(type) as Trace<TraceEntryTypeMap[T]['legacy']> | undefined;
}
deleteTrace<T extends TraceType>(type: T) {
diff --git a/tools/winscope/src/trace/traces_test.ts b/tools/winscope/src/trace/traces_test.ts
index ef473cf..64344c3 100644
--- a/tools/winscope/src/trace/traces_test.ts
+++ b/tools/winscope/src/trace/traces_test.ts
@@ -20,9 +20,9 @@
import {TracesUtils} from 'test/unit/traces_utils';
import {TraceBuilder} from 'test/unit/trace_builder';
import {TraceUtils} from 'test/unit/trace_utils';
+import {RealTimestamp} from '../common/time';
import {FrameMapBuilder} from './frame_map_builder';
import {AbsoluteFrameIndex} from './index_types';
-import {RealTimestamp} from './timestamp';
import {Traces} from './traces';
import {TraceType} from './trace_type';
diff --git a/tools/winscope/src/trace_collection/connection.ts b/tools/winscope/src/trace_collection/connection.ts
index b91f1d2..5ff46c9 100644
--- a/tools/winscope/src/trace_collection/connection.ts
+++ b/tools/winscope/src/trace_collection/connection.ts
@@ -13,18 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {ProxyClient} from 'trace_collection/proxy_client';
+import {Device, DeviceProperties, ProxyClient} from 'trace_collection/proxy_client';
import {ConfigMap} from './trace_collection_utils';
-export interface Device {
- [key: string]: DeviceProperties;
-}
-
-export interface DeviceProperties {
- authorised: boolean;
- model: string;
-}
-
export interface Connection {
adbSuccess: () => boolean;
setProxyKey?(key: string): any;
diff --git a/tools/winscope/src/trace_collection/proxy_client.ts b/tools/winscope/src/trace_collection/proxy_client.ts
index 02d33f3..85deefc 100644
--- a/tools/winscope/src/trace_collection/proxy_client.ts
+++ b/tools/winscope/src/trace_collection/proxy_client.ts
@@ -16,9 +16,17 @@
import {OnProgressUpdateType} from 'common/function_utils';
import {PersistentStore} from 'common/persistent_store';
-import {Device} from './connection';
import {ConfigMap} from './trace_collection_utils';
+export interface Device {
+ [key: string]: DeviceProperties;
+}
+
+export interface DeviceProperties {
+ authorised: boolean;
+ model: string;
+}
+
export enum ProxyState {
ERROR = 0,
CONNECTING = 1,
@@ -254,7 +262,7 @@
// stores all the changing variables from proxy and sets up calls from ProxyRequest
export class ProxyClient {
readonly WINSCOPE_PROXY_URL = 'http://localhost:5544';
- readonly VERSION = '1.0';
+ readonly VERSION = '1.2';
state: ProxyState = ProxyState.CONNECTING;
stateChangeListeners: Array<{(param: ProxyState, errorText: string): void}> = [];
refresh_worker: NodeJS.Timer | null = null;
diff --git a/tools/winscope/src/trace_collection/proxy_connection.ts b/tools/winscope/src/trace_collection/proxy_connection.ts
index 231e0a5..d5a5389 100644
--- a/tools/winscope/src/trace_collection/proxy_connection.ts
+++ b/tools/winscope/src/trace_collection/proxy_connection.ts
@@ -15,8 +15,14 @@
*/
import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
-import {proxyClient, ProxyEndpoint, proxyRequest, ProxyState} from 'trace_collection/proxy_client';
-import {Connection, DeviceProperties} from './connection';
+import {
+ DeviceProperties,
+ proxyClient,
+ ProxyEndpoint,
+ proxyRequest,
+ ProxyState,
+} from 'trace_collection/proxy_client';
+import {Connection} from './connection';
import {ConfigMap} from './trace_collection_utils';
import {TracingConfig} from './tracing_config';
diff --git a/tools/winscope/src/trace_collection/trace_collection_utils.ts b/tools/winscope/src/trace_collection/trace_collection_utils.ts
index 5654616..b9767d6 100644
--- a/tools/winscope/src/trace_collection/trace_collection_utils.ts
+++ b/tools/winscope/src/trace_collection/trace_collection_utils.ts
@@ -178,12 +178,6 @@
run: true,
config: undefined,
},
- accessibility_trace: {
- name: 'Accessibility',
- isTraceCollection: undefined,
- run: false,
- config: undefined,
- },
transactions: {
name: 'Transaction',
isTraceCollection: undefined,
@@ -214,12 +208,17 @@
run: false,
config: undefined,
},
+ view_capture_traces: {
+ name: 'View Capture',
+ isTraceCollection: undefined,
+ run: false,
+ config: undefined,
+ },
};
export const TRACES: {[key: string]: TraceConfigurationMap} = {
default: {
window_trace: traceConfigurations['window_trace'],
- accessibility_trace: traceConfigurations['accessibility_trace'],
layers_trace: traceConfigurations['layers_trace'],
transactions: traceConfigurations['transactions'],
proto_log: traceConfigurations['proto_log'],
@@ -227,6 +226,7 @@
ime_tracing: traceConfigurations['ime_tracing'],
eventlog: traceConfigurations['eventlog'],
transition_traces: traceConfigurations['transition_traces'],
+ view_capture_trace: traceConfigurations['view_capture_traces'],
},
arc: {
wayland_trace: traceConfigurations['wayland_trace'],
diff --git a/tools/winscope/src/trace_processor/bigint_math.ts b/tools/winscope/src/trace_processor/bigint_math.ts
new file mode 100644
index 0000000..dd1e550
--- /dev/null
+++ b/tools/winscope/src/trace_processor/bigint_math.ts
@@ -0,0 +1,111 @@
+// Copyright (C) 2023 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.
+
+export class BigintMath {
+ static INT64_MAX: bigint = (2n ** 63n) - 1n;
+ static INT64_MIN: bigint = -(2n ** 63n);
+
+ // Returns the smallest integral power of 2 that is not smaller than n.
+ // If n is less than or equal to 0, returns 1.
+ static bitCeil(n: bigint): bigint {
+ let result = 1n;
+ while (result < n) {
+ result <<= 1n;
+ }
+ return result;
+ };
+
+ // Returns the largest integral power of 2 which is not greater than n.
+ // If n is less than or equal to 0, returns 1.
+ static bitFloor(n: bigint): bigint {
+ let result = 1n;
+ while ((result << 1n) <= n) {
+ result <<= 1n;
+ }
+ return result;
+ };
+
+ // Returns the largest integral value x where 2^x is not greater than n.
+ static log2(n: bigint): number {
+ let result = 1n;
+ let log2 = 0;
+ while ((result << 1n) <= n) {
+ result <<= 1n;
+ ++log2;
+ }
+ return log2;
+ }
+
+ // Returns the integral multiple of step which is closest to n.
+ // If step is less than or equal to 0, returns n.
+ static quant(n: bigint, step: bigint): bigint {
+ step = BigintMath.max(1n, step);
+ const halfStep = step / 2n;
+ return step * ((n + halfStep) / step);
+ }
+
+ // Returns the largest integral multiple of step which is not larger than n.
+ // If step is less than or equal to 0, returns n.
+ static quantFloor(n: bigint, step: bigint): bigint {
+ step = BigintMath.max(1n, step);
+ return step * (n / step);
+ }
+
+ // Returns the smallest integral multiple of step which is not smaller than n.
+ // If step is less than or equal to 0, returns n.
+ static quantCeil(n: bigint, step: bigint): bigint {
+ step = BigintMath.max(1n, step);
+ const remainder = n % step;
+ if (remainder === 0n) {
+ return n;
+ }
+ const quotient = n / step;
+ return (quotient + 1n) * step;
+ }
+
+ // Returns the greater of a and b.
+ static max(a: bigint, b: bigint): bigint {
+ return a > b ? a : b;
+ }
+
+ // Returns the smaller of a and b.
+ static min(a: bigint, b: bigint): bigint {
+ return a < b ? a : b;
+ }
+
+ // Returns the number of 1 bits in n.
+ static popcount(n: bigint): number {
+ if (n < 0n) {
+ throw Error(`Can\'t get popcount of negative number ${n}`);
+ }
+ let count = 0;
+ while (n) {
+ if (n & 1n) {
+ ++count;
+ }
+ n >>= 1n;
+ }
+ return count;
+ }
+
+ // Return the ratio between two bigints as a number.
+ static ratio(dividend: bigint, divisor: bigint): number {
+ return Number(dividend) / Number(divisor);
+ }
+
+ // Calculates the absolute value of a n.
+ static abs(n: bigint) {
+ return n < 0n ? -1n * n : n;
+ }
+}
diff --git a/tools/winscope/src/trace_processor/deferred.ts b/tools/winscope/src/trace_processor/deferred.ts
new file mode 100644
index 0000000..114fb0c
--- /dev/null
+++ b/tools/winscope/src/trace_processor/deferred.ts
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 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.
+
+// Promise wrapper with exposed resolve and reject callbacks.
+export interface Deferred<T> extends Promise<T> {
+ readonly resolve: (value?: T|PromiseLike<T>) => void;
+ readonly reject: (reason?: any) => void;
+}
+
+// Create a promise with exposed resolve and reject callbacks.
+export function defer<T>(): Deferred<T> {
+ let resolve = null as any;
+ let reject = null as any;
+ const p = new Promise((res, rej) => [resolve, reject] = [res, rej]);
+ return Object.assign(p, {resolve, reject}) as any;
+}
diff --git a/tools/winscope/src/trace_processor/engine.ts b/tools/winscope/src/trace_processor/engine.ts
new file mode 100644
index 0000000..865f37a
--- /dev/null
+++ b/tools/winscope/src/trace_processor/engine.ts
@@ -0,0 +1,438 @@
+// Copyright (C) 2018 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.
+
+import {defer, Deferred} from './deferred';
+import {assertExists, assertTrue} from './logging';
+import {perfetto} from '../../deps_build/trace_processor/ui/tsc/gen/protos';
+
+import {ProtoRingBuffer} from './proto_ring_buffer';
+import {
+ ComputeMetricArgs,
+ ComputeMetricResult,
+ DisableAndReadMetatraceResult,
+ QueryArgs,
+ ResetTraceProcessorArgs,
+} from './protos';
+import {
+ createQueryResult,
+ LONG,
+ LONG_NULL,
+ NUM,
+ QueryError,
+ QueryResult,
+ STR,
+ WritableQueryResult,
+} from './query_result';
+
+import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
+import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
+import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
+
+export interface LoadingTracker {
+ beginLoading(): void;
+ endLoading(): void;
+}
+
+export class NullLoadingTracker implements LoadingTracker {
+ beginLoading(): void {}
+ endLoading(): void {}
+}
+
+
+// This is used to skip the decoding of queryResult from protobufjs and deal
+// with it ourselves. See the comment below around `QueryResult.decode = ...`.
+interface QueryResultBypass {
+ rawQueryResult: Uint8Array;
+}
+
+export interface TraceProcessorConfig {
+ cropTrackEvents: boolean;
+ ingestFtraceInRawTable: boolean;
+ analyzeTraceProtoContent: boolean;
+}
+
+// Abstract interface of a trace proccessor.
+// This is the TypeScript equivalent of src/trace_processor/rpc.h.
+// There are two concrete implementations:
+// 1. WasmEngineProxy: creates a Wasm module and interacts over postMessage().
+// 2. HttpRpcEngine: connects to an external `trace_processor_shell --httpd`.
+// and interacts via fetch().
+// In both cases, we have a byte-oriented pipe to interact with TraceProcessor.
+// The derived class is only expected to deal with these two functions:
+// 1. Implement the abstract rpcSendRequestBytes() function, sending the
+// proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
+// 2. Call onRpcResponseBytes() when response data is received.
+export abstract class Engine {
+ abstract readonly id: string;
+ private _cpus?: number[];
+ private _numGpus?: number;
+ private loadingTracker: LoadingTracker;
+ private txSeqId = 0;
+ private rxSeqId = 0;
+ private rxBuf = new ProtoRingBuffer();
+ private pendingParses = new Array<Deferred<void>>();
+ private pendingEOFs = new Array<Deferred<void>>();
+ private pendingResetTraceProcessors = new Array<Deferred<void>>();
+ private pendingQueries = new Array<WritableQueryResult>();
+ private pendingRestoreTables = new Array<Deferred<void>>();
+ private pendingComputeMetrics = new Array<Deferred<ComputeMetricResult>>();
+ private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
+ private _isMetatracingEnabled = false;
+
+ constructor(tracker?: LoadingTracker) {
+ this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
+ }
+
+ // Called to send data to the TraceProcessor instance. This turns into a
+ // postMessage() or a HTTP request, depending on the Engine implementation.
+ abstract rpcSendRequestBytes(data: Uint8Array): void;
+
+ // Called when an inbound message is received by the Engine implementation
+ // (e.g. onmessage for the Wasm case, on when HTTP replies are received for
+ // the HTTP+RPC case).
+ onRpcResponseBytes(dataWillBeRetained: Uint8Array) {
+ // Note: when hitting the fastpath inside ProtoRingBuffer, the |data| buffer
+ // is returned back by readMessage() (% subarray()-ing it) and held onto by
+ // other classes (e.g., QueryResult). For both fetch() and Wasm we are fine
+ // because every response creates a new buffer.
+ this.rxBuf.append(dataWillBeRetained);
+ for (;;) {
+ const msg = this.rxBuf.readMessage();
+ if (msg === undefined) break;
+ this.onRpcResponseMessage(msg);
+ }
+ }
+
+ // Parses a response message.
+ // |rpcMsgEncoded| is a sub-array to to the start of a TraceProcessorRpc
+ // proto-encoded message (without the proto preamble and varint size).
+ private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) {
+ // Here we override the protobufjs-generated code to skip the parsing of the
+ // new streaming QueryResult and instead passing it through like a buffer.
+ // This is the overall problem: All trace processor responses are wrapped
+ // into a perfetto.protos.TraceProcessorRpc proto message. In all cases %
+ // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and
+ // give us a structured object. In the case of TPM_QUERY_STREAMING, instead,
+ // we want to deal with the proto parsing ourselves using the new
+ // QueryResult.appendResultBatch() method, because that handled streaming
+ // results more efficiently and skips several copies.
+ // By overriding the decode method below, we achieve two things:
+ // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
+ // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
+ // the `case TPM_QUERY_STREAMING` below can take it.
+ perfetto.protos.QueryResult.decode =
+ (reader: protobuf.Reader, length: number) => {
+ const res =
+ perfetto.protos.QueryResult.create() as {} as QueryResultBypass;
+ res.rawQueryResult =
+ reader.buf.subarray(reader.pos, reader.pos + length);
+ // All this works only if protobufjs returns the original ArrayBuffer
+ // from |rpcMsgEncoded|. It should be always the case given the
+ // current implementation. This check mainly guards against future
+ // behavioral changes of protobufjs. We don't want to accidentally
+ // hold onto some internal protobufjs buffer. We are fine holding
+ // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
+ // is buffer-retention-friendly.
+ assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
+ reader.pos += length;
+ return res as {} as perfetto.protos.QueryResult;
+ };
+
+ const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
+
+ if (rpc.fatalError !== undefined && rpc.fatalError.length > 0) {
+ throw new Error(`${rpc.fatalError}`);
+ }
+
+ // Allow restarting sequences from zero (when reloading the browser).
+ if (rpc.seq !== this.rxSeqId + 1 && this.rxSeqId !== 0 && rpc.seq !== 0) {
+ // "(ERR:rpc_seq)" is intercepted by error_dialog.ts to show a more
+ // graceful and actionable error.
+ throw new Error(`RPC sequence id mismatch cur=${rpc.seq} last=${
+ this.rxSeqId} (ERR:rpc_seq)`);
+ }
+
+ this.rxSeqId = rpc.seq;
+
+ let isFinalResponse = true;
+
+ switch (rpc.response) {
+ case TPM.TPM_APPEND_TRACE_DATA:
+ const appendResult = assertExists(rpc.appendResult);
+ const pendingPromise = assertExists(this.pendingParses.shift());
+ if (appendResult.error && appendResult.error.length > 0) {
+ pendingPromise.reject(appendResult.error);
+ } else {
+ pendingPromise.resolve();
+ }
+ break;
+ case TPM.TPM_FINALIZE_TRACE_DATA:
+ assertExists(this.pendingEOFs.shift()).resolve();
+ break;
+ case TPM.TPM_RESET_TRACE_PROCESSOR:
+ assertExists(this.pendingResetTraceProcessors.shift()).resolve();
+ break;
+ case TPM.TPM_RESTORE_INITIAL_TABLES:
+ assertExists(this.pendingRestoreTables.shift()).resolve();
+ break;
+ case TPM.TPM_QUERY_STREAMING:
+ const qRes = assertExists(rpc.queryResult) as {} as QueryResultBypass;
+ const pendingQuery = assertExists(this.pendingQueries[0]);
+ pendingQuery.appendResultBatch(qRes.rawQueryResult);
+ if (pendingQuery.isComplete()) {
+ this.pendingQueries.shift();
+ } else {
+ isFinalResponse = false;
+ }
+ break;
+ case TPM.TPM_COMPUTE_METRIC:
+ const metricRes = assertExists(rpc.metricResult) as ComputeMetricResult;
+ const pendingComputeMetric =
+ assertExists(this.pendingComputeMetrics.shift());
+ if (metricRes.error && metricRes.error.length > 0) {
+ const error =
+ new QueryError(`ComputeMetric() error: ${metricRes.error}`, {
+ query: 'COMPUTE_METRIC',
+ });
+ pendingComputeMetric.reject(error);
+ } else {
+ pendingComputeMetric.resolve(metricRes);
+ }
+ break;
+ case TPM.TPM_DISABLE_AND_READ_METATRACE:
+ const metatraceRes =
+ assertExists(rpc.metatrace) as DisableAndReadMetatraceResult;
+ assertExists(this.pendingReadMetatrace).resolve(metatraceRes);
+ this.pendingReadMetatrace = undefined;
+ break;
+ default:
+ console.log(
+ 'Unexpected TraceProcessor response received: ', rpc.response);
+ break;
+ } // switch(rpc.response);
+
+ if (isFinalResponse) {
+ this.loadingTracker.endLoading();
+ }
+ }
+
+ // TraceProcessor methods below this point.
+ // The methods below are called by the various controllers in the UI and
+ // deal with marshalling / unmarshaling requests to/from TraceProcessor.
+
+
+ // Push trace data into the engine. The engine is supposed to automatically
+ // figure out the type of the trace (JSON vs Protobuf).
+ parse(data: Uint8Array): Promise<void> {
+ const asyncRes = defer<void>();
+ this.pendingParses.push(asyncRes);
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_APPEND_TRACE_DATA;
+ rpc.appendTraceData = data;
+ this.rpcSendRequest(rpc);
+ return asyncRes; // Linearize with the worker.
+ }
+
+ // Notify the engine that we reached the end of the trace.
+ // Called after the last parse() call.
+ notifyEof(): Promise<void> {
+ const asyncRes = defer<void>();
+ this.pendingEOFs.push(asyncRes);
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_FINALIZE_TRACE_DATA;
+ this.rpcSendRequest(rpc);
+ return asyncRes; // Linearize with the worker.
+ }
+
+ // Updates the TraceProcessor Config. This method creates a new
+ // TraceProcessor instance, so it should be called before passing any trace
+ // data.
+ resetTraceProcessor(
+ {cropTrackEvents, ingestFtraceInRawTable, analyzeTraceProtoContent}:
+ TraceProcessorConfig): Promise<void> {
+ const asyncRes = defer<void>();
+ this.pendingResetTraceProcessors.push(asyncRes);
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_RESET_TRACE_PROCESSOR;
+ const args = rpc.resetTraceProcessorArgs = new ResetTraceProcessorArgs();
+ args.dropTrackEventDataBefore = cropTrackEvents ?
+ ResetTraceProcessorArgs.DropTrackEventDataBefore
+ .TRACK_EVENT_RANGE_OF_INTEREST :
+ ResetTraceProcessorArgs.DropTrackEventDataBefore.NO_DROP;
+ args.ingestFtraceInRawTable = ingestFtraceInRawTable;
+ args.analyzeTraceProtoContent = analyzeTraceProtoContent;
+ this.rpcSendRequest(rpc);
+ return asyncRes;
+ }
+
+ // Resets the trace processor state by destroying any table/views created by
+ // the UI after loading.
+ restoreInitialTables(): Promise<void> {
+ const asyncRes = defer<void>();
+ this.pendingRestoreTables.push(asyncRes);
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_RESTORE_INITIAL_TABLES;
+ this.rpcSendRequest(rpc);
+ return asyncRes; // Linearize with the worker.
+ }
+
+ // Shorthand for sending a compute metrics request to the engine.
+ async computeMetric(metrics: string[]): Promise<ComputeMetricResult> {
+ const asyncRes = defer<ComputeMetricResult>();
+ this.pendingComputeMetrics.push(asyncRes);
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_COMPUTE_METRIC;
+ const args = rpc.computeMetricArgs = new ComputeMetricArgs();
+ args.metricNames = metrics;
+ args.format = ComputeMetricArgs.ResultFormat.TEXTPROTO;
+ this.rpcSendRequest(rpc);
+ return asyncRes;
+ }
+
+ // Issues a streaming query and retrieve results in batches.
+ // The returned QueryResult object will be populated over time with batches
+ // of rows (each batch conveys ~128KB of data and a variable number of rows).
+ // The caller can decide whether to wait that all batches have been received
+ // (by awaiting the returned object or calling result.waitAllRows()) or handle
+ // the rows incrementally.
+ //
+ // Example usage:
+ // const res = engine.query('SELECT foo, bar FROM table');
+ // console.log(res.numRows()); // Will print 0 because we didn't await.
+ // await(res.waitAllRows());
+ // console.log(res.numRows()); // Will print the total number of rows.
+ //
+ // for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) {
+ // console.log(it.foo, it.bar);
+ // }
+ //
+ // Optional |tag| (usually a component name) can be provided to allow
+ // attributing trace processor workload to different UI components.
+ query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_QUERY_STREAMING;
+ rpc.queryArgs = new QueryArgs();
+ rpc.queryArgs.sqlQuery = sqlQuery;
+ if (tag) {
+ rpc.queryArgs.tag = tag;
+ }
+ const result = createQueryResult({
+ query: sqlQuery,
+ });
+ this.pendingQueries.push(result);
+ this.rpcSendRequest(rpc);
+ return result;
+ }
+
+ isMetatracingEnabled(): boolean {
+ return this._isMetatracingEnabled;
+ }
+
+ enableMetatrace(categories?: perfetto.protos.MetatraceCategories) {
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_ENABLE_METATRACE;
+ if (categories) {
+ rpc.enableMetatraceArgs = new perfetto.protos.EnableMetatraceArgs();
+ rpc.enableMetatraceArgs.categories = categories;
+ }
+ this._isMetatracingEnabled = true;
+ this.rpcSendRequest(rpc);
+ }
+
+ stopAndGetMetatrace(): Promise<DisableAndReadMetatraceResult> {
+ // If we are already finalising a metatrace, ignore the request.
+ if (this.pendingReadMetatrace) {
+ return Promise.reject(new Error('Already finalising a metatrace'));
+ }
+
+ const result = defer<DisableAndReadMetatraceResult>();
+
+ const rpc = TraceProcessorRpc.create();
+ rpc.request = TPM.TPM_DISABLE_AND_READ_METATRACE;
+ this._isMetatracingEnabled = false;
+ this.pendingReadMetatrace = result;
+ this.rpcSendRequest(rpc);
+ return result;
+ }
+
+ // Marshals the TraceProcessorRpc request arguments and sends the request
+ // to the concrete Engine (Wasm or HTTP).
+ private rpcSendRequest(rpc: TraceProcessorRpc) {
+ rpc.seq = this.txSeqId++;
+ // Each message is wrapped in a TraceProcessorRpcStream to add the varint
+ // preamble with the size, which allows tokenization on the other end.
+ const outerProto = TraceProcessorRpcStream.create();
+ outerProto.msg.push(rpc);
+ const buf = TraceProcessorRpcStream.encode(outerProto).finish();
+ this.loadingTracker.beginLoading();
+ this.rpcSendRequestBytes(buf);
+ }
+
+ // TODO(hjd): When streaming must invalidate this somehow.
+ async getCpus(): Promise<number[]> {
+ if (!this._cpus) {
+ const cpus = [];
+ const queryRes = await this.query(
+ 'select distinct(cpu) as cpu from sched order by cpu;');
+ for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+ cpus.push(it.cpu);
+ }
+ this._cpus = cpus;
+ }
+ return this._cpus;
+ }
+
+ async getNumberOfGpus(): Promise<number> {
+ if (!this._numGpus) {
+ const result = await this.query(`
+ select count(distinct(gpu_id)) as gpuCount
+ from gpu_counter_track
+ where name = 'gpufreq';
+ `);
+ this._numGpus = result.firstRow({gpuCount: NUM}).gpuCount;
+ }
+ return this._numGpus;
+ }
+
+ // TODO: This should live in code that's more specific to chrome, instead of
+ // in engine.
+ async getNumberOfProcesses(): Promise<number> {
+ const result = await this.query('select count(*) as cnt from process;');
+ return result.firstRow({cnt: NUM}).cnt;
+ }
+
+ getProxy(tag: string): EngineProxy {
+ return new EngineProxy(this, tag);
+ }
+}
+
+// Lightweight wrapper over Engine exposing only `query` method and annotating
+// all queries going through it with a tag.
+export class EngineProxy {
+ private engine: Engine;
+ private tag: string;
+
+ constructor(engine: Engine, tag: string) {
+ this.engine = engine;
+ this.tag = tag;
+ }
+
+ query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
+ return this.engine.query(sqlQuery, tag || this.tag);
+ }
+
+ get engineId(): string {
+ return this.engine.id;
+ }
+}
diff --git a/tools/winscope/src/trace_processor/logging.ts b/tools/winscope/src/trace_processor/logging.ts
new file mode 100644
index 0000000..665488f
--- /dev/null
+++ b/tools/winscope/src/trace_processor/logging.ts
@@ -0,0 +1,77 @@
+// Copyright (C) 2018 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.
+
+import {SCM_REVISION, VERSION} from '../../deps_build/trace_processor/ui/tsc/gen/perfetto_version';
+
+export type ErrorHandler = (err: string) => void;
+
+let errorHandler: ErrorHandler = (_: string) => {};
+
+export function assertExists<A>(value: A | null | undefined): A {
+ if (value === null || value === undefined) {
+ throw new Error('Value doesn\'t exist');
+ }
+ return value;
+}
+
+export function assertTrue(value: boolean, optMsg?: string) {
+ if (!value) {
+ throw new Error(optMsg ?? 'Failed assertion');
+ }
+}
+
+export function assertFalse(value: boolean, optMsg?: string) {
+ assertTrue(!value, optMsg);
+}
+
+export function setErrorHandler(handler: ErrorHandler) {
+ errorHandler = handler;
+}
+
+export function reportError(err: ErrorEvent|PromiseRejectionEvent|{}) {
+ let errLog = '';
+ let errorObj = undefined;
+
+ if (err instanceof ErrorEvent) {
+ errLog = err.message;
+ errorObj = err.error;
+ } else if (err instanceof PromiseRejectionEvent) {
+ errLog = `${err.reason}`;
+ errorObj = err.reason;
+ } else {
+ errLog = `${err}`;
+ }
+ if (errorObj !== undefined && errorObj !== null) {
+ const errStack = (errorObj as {stack?: string}).stack;
+ errLog += '\n';
+ errLog += errStack !== undefined ? errStack : JSON.stringify(errorObj);
+ }
+ errLog += '\n\n';
+ errLog += `${VERSION} ${SCM_REVISION}\n`;
+ errLog += `UA: ${navigator.userAgent}\n`;
+
+ console.error(errLog, err);
+ errorHandler(errLog);
+}
+
+// This function serves two purposes.
+// 1) A runtime check - if we are ever called, we throw an exception.
+// This is useful for checking that code we suspect should never be reached is
+// actually never reached.
+// 2) A compile time check where typescript asserts that the value passed can be
+// cast to the "never" type.
+// This is useful for ensuring we exhastively check union types.
+export function assertUnreachable(_x: never) {
+ throw new Error('This code should not be reachable');
+}
diff --git a/tools/winscope/src/trace_processor/proto_ring_buffer.ts b/tools/winscope/src/trace_processor/proto_ring_buffer.ts
new file mode 100644
index 0000000..da878ce
--- /dev/null
+++ b/tools/winscope/src/trace_processor/proto_ring_buffer.ts
@@ -0,0 +1,156 @@
+// Copyright (C) 2021 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.
+
+import {assertTrue} from './logging';
+
+// This class is the TypeScript equivalent of the identically-named C++ class in
+// //protozero/proto_ring_buffer.h. See comments in that header for a detailed
+// description. The architecture is identical.
+
+const kGrowBytes = 128 * 1024;
+const kMaxMsgSize = 64 * 1024 * 1024;
+
+export class ProtoRingBuffer {
+ private buf = new Uint8Array(kGrowBytes);
+ private fastpath?: Uint8Array;
+ private rd = 0;
+ private wr = 0;
+
+ // The caller must call ReadMessage() after each append() call.
+ // The |data| might be either copied in the internal ring buffer or returned
+ // (% subarray()) to the next ReadMessage() call.
+ append(data: Uint8Array) {
+ assertTrue(this.wr <= this.buf.length);
+ assertTrue(this.rd <= this.wr);
+
+ // If the last call to ReadMessage() consumed all the data in the buffer and
+ // there are no incomplete messages pending, restart from the beginning
+ // rather than keep ringing. This is the most common case.
+ if (this.rd === this.wr) {
+ this.rd = this.wr = 0;
+ }
+
+ // The caller is expected to issue a ReadMessage() after each append().
+ const dataLen = data.length;
+ if (dataLen === 0) return;
+ assertTrue(this.fastpath === undefined);
+ if (this.rd === this.wr) {
+ const msg = ProtoRingBuffer.tryReadMessage(data, 0, dataLen);
+ if (msg !== undefined &&
+ ((msg.byteOffset + msg.length) === (data.byteOffset + dataLen))) {
+ // Fastpath: in many cases, the underlying stream will effectively
+ // preserve the atomicity of messages for most small messages.
+ // In this case we can avoid the extra buffer roundtrip and return the
+ // original array (actually a subarray that skips the proto header).
+ // The next call to ReadMessage() will return this.
+ this.fastpath = msg;
+ return;
+ }
+ }
+
+ let avail = this.buf.length - this.wr;
+ if (dataLen > avail) {
+ // This whole section should be hit extremely rarely.
+
+ // Try first just recompacting the buffer by moving everything to the
+ // left. This can happen if we received "a message and a bit" on each
+ // append() call.
+ this.buf.copyWithin(0, this.rd, this.wr);
+ avail += this.rd;
+ this.wr -= this.rd;
+ this.rd = 0;
+ if (dataLen > avail) {
+ // Still not enough, expand the buffer.
+ let newSize = this.buf.length;
+ while (dataLen > newSize - this.wr) {
+ newSize += kGrowBytes;
+ }
+ assertTrue(newSize <= kMaxMsgSize * 2);
+ const newBuf = new Uint8Array(newSize);
+ newBuf.set(this.buf);
+ this.buf = newBuf;
+ // No need to touch rd / wr.
+ }
+ }
+
+ // Append the received data at the end of the ring buffer.
+ this.buf.set(data, this.wr);
+ this.wr += dataLen;
+ }
+
+ // Tries to extract a message from the ring buffer. If there is no message,
+ // or if the current message is still incomplete, returns undefined.
+ // The caller is expected to call this in a loop until it returns undefined.
+ // Note that a single write to Append() can yield more than one message
+ // (see ProtoRingBufferTest.CoalescingStream in the unittest).
+ readMessage(): Uint8Array|undefined {
+ if (this.fastpath !== undefined) {
+ assertTrue(this.rd === this.wr);
+ const msg = this.fastpath;
+ this.fastpath = undefined;
+ return msg;
+ }
+ assertTrue(this.rd <= this.wr);
+ if (this.rd >= this.wr) {
+ return undefined; // Completely empty.
+ }
+ const msg = ProtoRingBuffer.tryReadMessage(this.buf, this.rd, this.wr);
+ if (msg === undefined) return undefined;
+ assertTrue(msg.buffer === this.buf.buffer);
+ assertTrue(this.buf.byteOffset === 0);
+ this.rd = msg.byteOffset + msg.length;
+
+ // Deliberately returning a copy of the data with slice(). In various cases
+ // (streaming query response) the caller will hold onto the returned buffer.
+ // If we get to this point, |msg| is a view of the circular buffer that we
+ // will overwrite on the next calls to append().
+ return msg.slice();
+ }
+
+ private static tryReadMessage(
+ data: Uint8Array, dataStart: number, dataEnd: number): Uint8Array
+ |undefined {
+ assertTrue(dataEnd <= data.length);
+ let pos = dataStart;
+ if (pos >= dataEnd) return undefined;
+ const tag = data[pos++]; // Assume one-byte tag.
+ if (tag >= 0x80 || (tag & 0x07) !== 2 /* len delimited */) {
+ throw new Error(
+ `RPC framing error, unexpected tag ${tag} @ offset ${pos - 1}`);
+ }
+
+ let len = 0;
+ for (let shift = 0; /* no check */; shift += 7) {
+ if (pos >= dataEnd) {
+ return undefined; // Not enough data to read varint.
+ }
+ const val = data[pos++];
+ len |= ((val & 0x7f) << shift) >>> 0;
+ if (val < 0x80) break;
+ }
+
+ if (len >= kMaxMsgSize) {
+ throw new Error(
+ `RPC framing error, message too large (${len} > ${kMaxMsgSize}`);
+ }
+ const end = pos + len;
+ if (end > dataEnd) return undefined;
+
+ // This is a subarray() and not a slice() because in the |fastpath| case
+ // we want to just return the original buffer pushed by append().
+ // In the slow-path (ring-buffer) case, the readMessage() above will create
+ // a copy via slice() before returning it.
+ return data.subarray(pos, end);
+ }
+}
diff --git a/tools/winscope/src/trace_processor/protos.ts b/tools/winscope/src/trace_processor/protos.ts
new file mode 100644
index 0000000..f042bf2
--- /dev/null
+++ b/tools/winscope/src/trace_processor/protos.ts
@@ -0,0 +1,134 @@
+// Copyright (C) 2018 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.
+
+import protos from '../../deps_build/trace_processor/ui/tsc/gen/protos';
+
+// Aliases protos to avoid the super nested namespaces.
+// See https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases
+import AndroidLogConfig = protos.perfetto.protos.AndroidLogConfig;
+import AndroidPowerConfig = protos.perfetto.protos.AndroidPowerConfig;
+import AndroidLogId = protos.perfetto.protos.AndroidLogId;
+import BatteryCounters =
+ protos.perfetto.protos.AndroidPowerConfig.BatteryCounters;
+import BufferConfig = protos.perfetto.protos.TraceConfig.BufferConfig;
+import ChromeConfig = protos.perfetto.protos.ChromeConfig;
+import TrackEventConfig = protos.perfetto.protos.TrackEventConfig;
+import ConsumerPort = protos.perfetto.protos.ConsumerPort;
+import NetworkPacketTraceConfig =
+ protos.perfetto.protos.NetworkPacketTraceConfig;
+import NativeContinuousDumpConfig =
+ protos.perfetto.protos.HeapprofdConfig.ContinuousDumpConfig;
+import JavaContinuousDumpConfig =
+ protos.perfetto.protos.JavaHprofConfig.ContinuousDumpConfig;
+import DataSourceConfig = protos.perfetto.protos.DataSourceConfig;
+import DataSourceDescriptor = protos.perfetto.protos.DataSourceDescriptor;
+import FtraceConfig = protos.perfetto.protos.FtraceConfig;
+import HeapprofdConfig = protos.perfetto.protos.HeapprofdConfig;
+import JavaHprofConfig = protos.perfetto.protos.JavaHprofConfig;
+import IAndroidPowerConfig = protos.perfetto.protos.IAndroidPowerConfig;
+import IBufferConfig = protos.perfetto.protos.TraceConfig.IBufferConfig;
+import IProcessStatsConfig = protos.perfetto.protos.IProcessStatsConfig;
+import ISysStatsConfig = protos.perfetto.protos.ISysStatsConfig;
+import ITraceConfig = protos.perfetto.protos.ITraceConfig;
+import MeminfoCounters = protos.perfetto.protos.MeminfoCounters;
+import ProcessStatsConfig = protos.perfetto.protos.ProcessStatsConfig;
+import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
+import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
+import TraceConfig = protos.perfetto.protos.TraceConfig;
+import VmstatCounters = protos.perfetto.protos.VmstatCounters;
+import IPCFrame = protos.perfetto.protos.IPCFrame;
+import IMethodInfo =
+ protos.perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
+import IBufferStats = protos.perfetto.protos.TraceStats.IBufferStats;
+import ISlice = protos.perfetto.protos.ReadBuffersResponse.ISlice;
+import EnableTracingRequest = protos.perfetto.protos.EnableTracingRequest;
+import DisableTracingRequest = protos.perfetto.protos.DisableTracingRequest;
+import GetTraceStatsRequest = protos.perfetto.protos.GetTraceStatsRequest;
+import FreeBuffersRequest = protos.perfetto.protos.FreeBuffersRequest;
+import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
+import QueryServiceStateRequest =
+ protos.perfetto.protos.QueryServiceStateRequest;
+import EnableTracingResponse = protos.perfetto.protos.EnableTracingResponse;
+import DisableTracingResponse = protos.perfetto.protos.DisableTracingResponse;
+import GetTraceStatsResponse = protos.perfetto.protos.GetTraceStatsResponse;
+import FreeBuffersResponse = protos.perfetto.protos.FreeBuffersResponse;
+import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
+import QueryServiceStateResponse =
+ protos.perfetto.protos.QueryServiceStateResponse;
+// Trace Processor protos.
+import QueryArgs = protos.perfetto.protos.QueryArgs;
+import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
+import StatusResult = protos.perfetto.protos.StatusResult;
+import ComputeMetricArgs = protos.perfetto.protos.ComputeMetricArgs;
+import ComputeMetricResult = protos.perfetto.protos.ComputeMetricResult;
+import DisableAndReadMetatraceResult =
+ protos.perfetto.protos.DisableAndReadMetatraceResult;
+import Trace = protos.perfetto.protos.Trace;
+import TracePacket = protos.perfetto.protos.TracePacket;
+import PerfettoMetatrace = protos.perfetto.protos.PerfettoMetatrace;
+
+export {
+ AndroidLogConfig,
+ AndroidLogId,
+ AndroidPowerConfig,
+ BatteryCounters,
+ BufferConfig,
+ ChromeConfig,
+ ConsumerPort,
+ ComputeMetricArgs,
+ ComputeMetricResult,
+ DataSourceConfig,
+ DisableAndReadMetatraceResult,
+ DataSourceDescriptor,
+ DisableTracingRequest,
+ DisableTracingResponse,
+ EnableTracingRequest,
+ EnableTracingResponse,
+ FreeBuffersRequest,
+ FreeBuffersResponse,
+ FtraceConfig,
+ GetTraceStatsRequest,
+ GetTraceStatsResponse,
+ HeapprofdConfig,
+ IAndroidPowerConfig,
+ IBufferConfig,
+ IBufferStats,
+ IMethodInfo,
+ IPCFrame,
+ IProcessStatsConfig,
+ ISlice,
+ ISysStatsConfig,
+ ITraceConfig,
+ JavaContinuousDumpConfig,
+ JavaHprofConfig,
+ MeminfoCounters,
+ NativeContinuousDumpConfig,
+ NetworkPacketTraceConfig,
+ ProcessStatsConfig,
+ PerfettoMetatrace,
+ ReadBuffersRequest,
+ ReadBuffersResponse,
+ QueryServiceStateRequest,
+ QueryServiceStateResponse,
+ QueryArgs,
+ ResetTraceProcessorArgs,
+ StatCounters,
+ StatusResult,
+ SysStatsConfig,
+ Trace,
+ TraceConfig,
+ TrackEventConfig,
+ TracePacket,
+ VmstatCounters,
+};
diff --git a/tools/winscope/src/trace_processor/query_result.ts b/tools/winscope/src/trace_processor/query_result.ts
new file mode 100644
index 0000000..0185cf1
--- /dev/null
+++ b/tools/winscope/src/trace_processor/query_result.ts
@@ -0,0 +1,963 @@
+// Copyright (C) 2021 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.
+
+// This file deals with deserialization and iteration of the proto-encoded
+// byte buffer that is returned by TraceProcessor when invoking the
+// TPM_QUERY_STREAMING method. The returned |query_result| buffer is optimized
+// for being moved cheaply across workers and decoded on-the-flight as we step
+// through the iterator.
+// See comments around QueryResult in trace_processor.proto for more details.
+
+// The classes in this file are organized as follows:
+//
+// QueryResultImpl:
+// The object returned by the Engine.query(sql) method.
+// This object is a holder of row data. Batches of raw get appended
+// incrementally as they are received by the remote TraceProcessor instance.
+// QueryResultImpl also deals with asynchronicity of queries and allows callers
+// to obtain a promise that waits for more (or all) rows.
+// At any point in time the following objects hold a reference to QueryResult:
+// - The Engine: for appending row batches.
+// - UI code, typically controllers, who make queries.
+//
+// ResultBatch:
+// Hold the data, returned by the remote TraceProcessor instance, for a number
+// of rows (TP typically chunks the results in batches of 128KB).
+// A QueryResultImpl holds exclusively ResultBatches for a given query.
+// ResultBatch is not exposed externally, it's just an internal representation
+// that helps with proto decoding. ResultBatch is immutable after it gets
+// appended and decoded. The iteration state is held by the RowIteratorImpl.
+//
+// RowIteratorImpl:
+// Decouples the data owned by QueryResultImpl (and its ResultBatch(es)) from
+// the iteration state. The iterator effectively is the union of a ResultBatch
+// and the row number in it. Rows within the batch are decoded as the user calls
+// next(). When getting at the end of the batch, it takes care of switching to
+// the next batch (if any) within the QueryResultImpl.
+// This object is part of the API exposed to tracks / controllers.
+
+// Import below commented out to prevent the protobufjs initialiation with:
+// `protobuf.util.Long = undefined as any;`
+// The Winscope parsers need the 64-bit proto fields to be retrieved as Long instead of number,
+// otherwise data (e.g. state flags) would be lost because of the 53-bit integer limitation.
+// import './static_initializers';
+
+import protobuf from 'protobufjs/minimal';
+
+import {defer, Deferred} from './deferred';
+import {assertExists, assertFalse, assertTrue} from './logging';
+import {utf8Decode} from './string_utils';
+
+export const NUM = 0;
+export const STR = 'str';
+export const NUM_NULL: number|null = 1;
+export const STR_NULL: string|null = 'str_null';
+export const BLOB: Uint8Array = new Uint8Array();
+export const BLOB_NULL: Uint8Array|null = new Uint8Array();
+export const LONG: bigint = 0n;
+export const LONG_NULL: bigint|null = 1n;
+
+export type ColumnType = string|number|bigint|null|Uint8Array;
+export type SqlValue = ColumnType;
+
+const SHIFT_32BITS = 32n;
+
+// Fast decode varint int64 into a bigint
+// Inspired by
+// https://github.com/protobufjs/protobuf.js/blob/56b1e64979dae757b67a21d326e16acee39f2267/src/reader.js#L123
+export function decodeInt64Varint(buf: Uint8Array, pos: number): bigint {
+ let hi: number = 0;
+ let lo: number = 0;
+ let i = 0;
+
+ if (buf.length - pos > 4) { // fast route (lo)
+ for (; i < 4; ++i) {
+ // 1st..4th
+ lo = (lo | (buf[pos] & 127) << i * 7) >>> 0;
+ if (buf[pos++] < 128) {
+ return BigInt(lo);
+ }
+ }
+ // 5th
+ lo = (lo | (buf[pos] & 127) << 28) >>> 0;
+ hi = (hi | (buf[pos] & 127) >> 4) >>> 0;
+ if (buf[pos++] < 128) {
+ return BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+ }
+ i = 0;
+ } else {
+ for (; i < 3; ++i) {
+ if (pos >= buf.length) {
+ throw Error('Index out of range');
+ }
+ // 1st..3rd
+ lo = (lo | (buf[pos] & 127) << i * 7) >>> 0;
+ if (buf[pos++] < 128) {
+ return BigInt(lo);
+ }
+ }
+ // 4th
+ lo = (lo | (buf[pos++] & 127) << i * 7) >>> 0;
+ return BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+ }
+ if (buf.length - pos > 4) { // fast route (hi)
+ for (; i < 5; ++i) {
+ // 6th..10th
+ hi = (hi | (buf[pos] & 127) << i * 7 + 3) >>> 0;
+ if (buf[pos++] < 128) {
+ const big = BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+ return BigInt.asIntN(64, big);
+ }
+ }
+ } else {
+ for (; i < 5; ++i) {
+ if (pos >= buf.length) {
+ throw Error('Index out of range');
+ }
+ // 6th..10th
+ hi = (hi | (buf[pos] & 127) << i * 7 + 3) >>> 0;
+ if (buf[pos++] < 128) {
+ const big = BigInt(hi) << SHIFT_32BITS | BigInt(lo);
+ return BigInt.asIntN(64, big);
+ }
+ }
+ }
+ throw Error('invalid varint encoding');
+}
+
+// Info that could help debug a query error. For example the query
+// in question, the stack where the query was issued, the active
+// plugin etc.
+export interface QueryErrorInfo {
+ query: string;
+}
+
+export class QueryError extends Error {
+ readonly query: string;
+
+ constructor(message: string, info: QueryErrorInfo) {
+ super(message);
+ this.query = info.query;
+ }
+
+ override toString() {
+ return `Query: ${this.query}\n` + super.toString();
+ }
+}
+
+// One row extracted from an SQL result:
+export interface Row {
+ [key: string]: ColumnType;
+}
+
+// The methods that any iterator has to implement.
+export interface RowIteratorBase {
+ valid(): boolean;
+ next(): void;
+
+ // Reflection support for cases where the column names are not known upfront
+ // (e.g. the query result table for user-provided SQL queries).
+ // It throws if the passed column name doesn't exist.
+ // Example usage:
+ // for (const it = queryResult.iter({}); it.valid(); it.next()) {
+ // for (const columnName : queryResult.columns()) {
+ // console.log(it.get(columnName));
+ get(columnName: string): ColumnType;
+}
+
+// A RowIterator is a type that has all the fields defined in the query spec
+// plus the valid() and next() operators. This is to ultimately allow the
+// clients to do:
+// const result = await engine.query("select name, surname, id from people;");
+// const iter = queryResult.iter({name: STR, surname: STR, id: NUM});
+// for (; iter.valid(); iter.next())
+// console.log(iter.name, iter.surname);
+export type RowIterator<T extends Row> = RowIteratorBase&T;
+
+function columnTypeToString(t: ColumnType): string {
+ switch (t) {
+ case NUM:
+ return 'NUM';
+ case NUM_NULL:
+ return 'NUM_NULL';
+ case STR:
+ return 'STR';
+ case STR_NULL:
+ return 'STR_NULL';
+ case BLOB:
+ return 'BLOB';
+ case BLOB_NULL:
+ return 'BLOB_NULL';
+ case LONG:
+ return 'LONG';
+ case LONG_NULL:
+ return 'LONG_NULL';
+ default:
+ return `INVALID(${t})`;
+ }
+}
+
+function isCompatible(actual: CellType, expected: ColumnType): boolean {
+ switch (actual) {
+ case CellType.CELL_NULL:
+ return expected === NUM_NULL || expected === STR_NULL ||
+ expected === BLOB_NULL || expected === LONG_NULL;
+ case CellType.CELL_VARINT:
+ return expected === NUM || expected === NUM_NULL || expected === LONG ||
+ expected === LONG_NULL;
+ case CellType.CELL_FLOAT64:
+ return expected === NUM || expected === NUM_NULL;
+ case CellType.CELL_STRING:
+ return expected === STR || expected === STR_NULL;
+ case CellType.CELL_BLOB:
+ return expected === BLOB || expected === BLOB_NULL;
+ default:
+ throw new Error(`Unknown CellType ${actual}`);
+ }
+}
+
+// This has to match CellType in trace_processor.proto.
+enum CellType {
+ CELL_NULL = 1,
+ CELL_VARINT = 2,
+ CELL_FLOAT64 = 3,
+ CELL_STRING = 4,
+ CELL_BLOB = 5,
+}
+
+const CELL_TYPE_NAMES =
+ ['UNKNOWN', 'NULL', 'VARINT', 'FLOAT64', 'STRING', 'BLOB'];
+
+const TAG_LEN_DELIM = 2;
+
+// This is the interface exposed to readers (e.g. tracks). The underlying object
+// (QueryResultImpl) owns the result data. This allows to obtain iterators on
+// that. In future it will allow to wait for incremental updates (new rows being
+// fetched) for streaming queries.
+export interface QueryResult {
+ // Obtains an iterator.
+ // TODO(primiano): this should have an option to destruct data as we read. In
+ // the case of a long query (e.g. `SELECT * FROM sched` in the query prompt)
+ // we don't want to accumulate everything in memory. OTOH UI tracks want to
+ // keep the data around so they can redraw them on each animation frame. For
+ // now we keep everything in memory in the QueryResultImpl object.
+ // iter<T extends Row>(spec: T): RowIterator<T>;
+ iter<T extends Row>(spec: T): RowIterator<T>;
+
+ // Like iter() for queries that expect only one row. It embeds the valid()
+ // check (i.e. throws if no rows are available) and returns directly the
+ // first result.
+ firstRow<T extends Row>(spec: T): T;
+
+ // If != undefined the query errored out and error() contains the message.
+ error(): string|undefined;
+
+ // Returns the number of rows accumulated so far. Note that this number can
+ // change over time as more batches are received. It becomes stable only
+ // when isComplete() returns true or after waitAllRows() is resolved.
+ numRows(): number;
+
+ // If true all rows have been fetched. Calling iter() will iterate through the
+ // last row. If false, iter() will return an iterator which might iterate
+ // through some rows (or none) but will surely not reach the end.
+
+ isComplete(): boolean;
+
+ // Returns a promise that is resolved only when all rows (i.e. all batches)
+ // have been fetched. The promise return value is always the object iself.
+ waitAllRows(): Promise<QueryResult>;
+
+ // Returns a promise that is resolved when either:
+ // - more rows are available
+ // - all rows are available
+ // The promise return value is always the object iself.
+ waitMoreRows(): Promise<QueryResult>;
+
+ // Can return an empty array if called before the first batch is resolved.
+ // This should be called only after having awaited for at least one batch.
+ columns(): string[];
+
+ // Returns the number of SQL statements in the query
+ // (e.g. 2 'if SELECT 1; SELECT 2;')
+ statementCount(): number;
+
+ // Returns the number of SQL statement that produced output rows. This number
+ // is <= statementCount().
+ statementWithOutputCount(): number;
+
+ // Returns the last SQL statement.
+ lastStatementSql(): string;
+}
+
+// Interface exposed to engine.ts to pump in the data as new row batches arrive.
+export interface WritableQueryResult extends QueryResult {
+ // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+ // The overall flow looks as follows:
+ // - The user calls engine.query('select ...') and gets a QueryResult back.
+ // - The query call posts a message to the worker that runs the SQL engine (
+ // or sends a HTTP request in case of the RPC+HTTP interface).
+ // - The returned QueryResult object is initially empty.
+ // - Over time, the sql engine will postMessage() back results in batches.
+ // - Each bach will end up calling this appendResultBatch() method.
+ // - If there is any pending promise (e.g. the caller called
+ // queryResult.waitAllRows()), this call will awake them (if this is the
+ // last batch).
+ appendResultBatch(resBytes: Uint8Array): void;
+}
+
+// The actual implementation, which bridges together the reader side and the
+// writer side (the one exposed to the Engine). This is the same object so that
+// when the engine pumps new row batches we can resolve pending promises that
+// readers (e.g. track code) are waiting for.
+class QueryResultImpl implements QueryResult, WritableQueryResult {
+ columnNames: string[] = [];
+ private _error?: string;
+ private _numRows = 0;
+ private _isComplete = false;
+ private _errorInfo: QueryErrorInfo;
+ private _statementCount = 0;
+ private _statementWithOutputCount = 0;
+ private _lastStatementSql = '';
+
+ constructor(errorInfo: QueryErrorInfo) {
+ this._errorInfo = errorInfo;
+ }
+
+ // --- QueryResult implementation.
+
+ // TODO(primiano): for the moment new batches are appended but old batches
+ // are never removed. This won't work with abnormally large result sets, as
+ // it will stash all rows in memory. We could switch to a model where the
+ // iterator is destructive and deletes batch objects once iterating past the
+ // end of each batch. If we do that, than we need to assign monotonic IDs to
+ // batches. Also if we do that, we should prevent creating more than one
+ // iterator for a QueryResult.
+ batches: ResultBatch[] = [];
+
+ // Promise awaiting on waitAllRows(). This should be resolved only when the
+ // last result batch has been been retrieved.
+ private allRowsPromise?: Deferred<QueryResult>;
+
+ // Promise awaiting on waitMoreRows(). This resolved when the next
+ // batch is appended via appendResultBatch.
+ private moreRowsPromise?: Deferred<QueryResult>;
+
+ isComplete(): boolean {
+ return this._isComplete;
+ }
+ numRows(): number {
+ return this._numRows;
+ }
+ error(): string|undefined {
+ return this._error;
+ }
+ columns(): string[] {
+ return this.columnNames;
+ }
+ statementCount(): number {
+ return this._statementCount;
+ }
+ statementWithOutputCount(): number {
+ return this._statementWithOutputCount;
+ }
+ lastStatementSql(): string {
+ return this._lastStatementSql;
+ }
+
+ iter<T extends Row>(spec: T): RowIterator<T> {
+ const impl = new RowIteratorImplWithRowData(spec, this);
+ return impl as {} as RowIterator<T>;
+ }
+
+ firstRow<T extends Row>(spec: T): T {
+ const impl = new RowIteratorImplWithRowData(spec, this);
+ assertTrue(impl.valid());
+ return impl as {} as RowIterator<T>as T;
+ }
+
+ // Can be called only once.
+ waitAllRows(): Promise<QueryResult> {
+ assertTrue(this.allRowsPromise === undefined);
+ this.allRowsPromise = defer<QueryResult>();
+ if (this._isComplete) {
+ this.resolveOrReject(this.allRowsPromise, this);
+ }
+ return this.allRowsPromise;
+ }
+
+ waitMoreRows(): Promise<QueryResult> {
+ if (this.moreRowsPromise !== undefined) {
+ return this.moreRowsPromise;
+ }
+
+ const moreRowsPromise = defer<QueryResult>();
+ if (this._isComplete) {
+ this.resolveOrReject(moreRowsPromise, this);
+ } else {
+ this.moreRowsPromise = moreRowsPromise;
+ }
+ return moreRowsPromise;
+ }
+
+ // --- WritableQueryResult implementation.
+
+ // Called by the engine when a new QueryResult is available. Note that a
+ // single Query() call can yield >1 QueryResult due to result batching
+ // if more than ~64K of data are returned, e.g. when returning O(M) rows.
+ // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+ // It is fine to retain the resBytes without slicing a copy, because
+ // ProtoRingBuffer does the slice() for us (or passes through the buffer
+ // coming from postMessage() (Wasm case) of fetch() (HTTP+RPC case).
+ appendResultBatch(resBytes: Uint8Array) {
+ const reader = protobuf.Reader.create(resBytes);
+ assertTrue(reader.pos === 0);
+ const columnNamesEmptyAtStartOfBatch = this.columnNames.length === 0;
+ const columnNamesSet = new Set<string>();
+ while (reader.pos < reader.len) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: // column_names
+ // Only the first batch should contain the column names. If this fires
+ // something is going wrong in the handling of the batch stream.
+ assertTrue(columnNamesEmptyAtStartOfBatch);
+ const origColName = reader.string();
+ let colName = origColName;
+ // In some rare cases two columns can have the same name (b/194891824)
+ // e.g. `select 1 as x, 2 as x`. These queries don't happen in the
+ // UI code, but they can happen when the user types a query (e.g.
+ // with a join). The most practical thing we can do here is renaming
+ // the columns with a suffix. Keeping the same name will break when
+ // iterating, because column names become iterator object keys.
+ for (let i = 1; columnNamesSet.has(colName); ++i) {
+ colName = `${origColName}_${i}`;
+ assertTrue(i < 100); // Give up at some point;
+ }
+ columnNamesSet.add(colName);
+ this.columnNames.push(colName);
+ break;
+ case 2: // error
+ // The query has errored only if the |error| field is non-empty.
+ // In protos, we don't distinguish between non-present and empty.
+ // Make sure we don't propagate ambiguous empty strings to JS.
+ const err = reader.string();
+ this._error = (err !== undefined && err.length) ? err : undefined;
+ break;
+ case 3: // batch
+ const batchLen = reader.uint32();
+ const batchRaw = resBytes.subarray(reader.pos, reader.pos + batchLen);
+ reader.pos += batchLen;
+
+ // The ResultBatch ctor parses the CellsBatch submessage.
+ const parsedBatch = new ResultBatch(batchRaw);
+ this.batches.push(parsedBatch);
+ this._isComplete = parsedBatch.isLastBatch;
+
+ // In theory one could construct a valid proto serializing the column
+ // names after the cell batches. In practice the QueryResultSerializer
+ // doesn't do that so it's not worth complicating the code.
+ const numColumns = this.columnNames.length;
+ if (numColumns !== 0) {
+ assertTrue(parsedBatch.numCells % numColumns === 0);
+ this._numRows += parsedBatch.numCells / numColumns;
+ } else {
+ // numColumns == 0 is plausible for queries like CREATE TABLE ... .
+ assertTrue(parsedBatch.numCells === 0);
+ }
+ break;
+
+ case 4:
+ this._statementCount = reader.uint32();
+ break;
+
+ case 5:
+ this._statementWithOutputCount = reader.uint32();
+ break;
+
+ case 6:
+ this._lastStatementSql = reader.string();
+ break;
+
+ default:
+ console.warn(`Unexpected QueryResult field ${tag >>> 3}`);
+ reader.skipType(tag & 7);
+ break;
+ } // switch (tag)
+ } // while (pos < end)
+
+ if (this.moreRowsPromise !== undefined) {
+ this.resolveOrReject(this.moreRowsPromise, this);
+ this.moreRowsPromise = undefined;
+ }
+
+ if (this._isComplete && this.allRowsPromise !== undefined) {
+ this.resolveOrReject(this.allRowsPromise, this);
+ }
+ }
+
+ ensureAllRowsPromise(): Promise<QueryResult> {
+ if (this.allRowsPromise === undefined) {
+ this.waitAllRows(); // Will populate |this.allRowsPromise|.
+ }
+ return assertExists(this.allRowsPromise);
+ }
+
+ private resolveOrReject(promise: Deferred<QueryResult>, arg: QueryResult) {
+ if (this._error === undefined) {
+ promise.resolve(arg);
+ } else {
+ promise.reject(new QueryError(this._error, this._errorInfo));
+ }
+ }
+}
+
+// This class holds onto a received result batch (a Uint8Array) and does some
+// partial parsing to tokenize the various cell groups. This parsing mainly
+// consists of identifying and caching the offsets of each cell group and
+// initializing the varint decoders. This half parsing is done to keep the
+// iterator's next() fast, without decoding everything into memory.
+// This is an internal implementation detail and is not exposed outside. The
+// RowIteratorImpl uses this class to iterate through batches (this class takes
+// care of iterating within a batch, RowIteratorImpl takes care of switching
+// batches when needed).
+// Note: at any point in time there can be more than one ResultIterator
+// referencing the same batch. The batch must be immutable.
+class ResultBatch {
+ readonly isLastBatch: boolean = false;
+ readonly batchBytes: Uint8Array;
+ readonly cellTypesOff: number = 0;
+ readonly cellTypesLen: number = 0;
+ readonly varintOff: number = 0;
+ readonly varintLen: number = 0;
+ readonly float64Cells = new Float64Array();
+ readonly blobCells: Uint8Array[] = [];
+ readonly stringCells: string[] = [];
+
+ // batchBytes is a trace_processor.QueryResult.CellsBatch proto.
+ constructor(batchBytes: Uint8Array) {
+ this.batchBytes = batchBytes;
+ const reader = protobuf.Reader.create(batchBytes);
+ assertTrue(reader.pos === 0);
+ const end = reader.len;
+
+ // Here we deconstruct the proto by hand. The CellsBatch is carefully
+ // designed to allow a very fast parsing from the TS side. We pack all cells
+ // of the same types together, so we can do only one call (per batch) to
+ // TextDecoder.decode(), we can overlay a memory-aligned typedarray for
+ // float values and can quickly tell and type-check the cell types.
+ // One row = N cells (we know the number upfront from the outer message).
+ // Each bach contains always an integer multiple of N cells (i.e. rows are
+ // never fragmented across different batches).
+ while (reader.pos < end) {
+ const tag = reader.uint32();
+ switch (tag >>> 3) {
+ case 1: // cell types, a packed array containing one CellType per cell.
+ assertTrue((tag & 7) === TAG_LEN_DELIM); // Must be packed varint.
+ this.cellTypesLen = reader.uint32();
+ this.cellTypesOff = reader.pos;
+ reader.pos += this.cellTypesLen;
+ break;
+
+ case 2: // varint_cells, a packed varint buffer.
+ assertTrue((tag & 7) === TAG_LEN_DELIM); // Must be packed varint.
+ const packLen = reader.uint32();
+ this.varintOff = reader.pos;
+ this.varintLen = packLen;
+ assertTrue(reader.buf === batchBytes);
+ assertTrue(
+ this.varintOff + this.varintLen <=
+ batchBytes.byteOffset + batchBytes.byteLength);
+ reader.pos += packLen;
+ break;
+
+ case 3: // float64_cells, a 64-bit aligned packed fixed64 buffer.
+ assertTrue((tag & 7) === TAG_LEN_DELIM); // Must be packed varint.
+ const f64Len = reader.uint32();
+ assertTrue(f64Len % 8 === 0);
+ // Float64Array's constructor is evil: the offset is in bytes but the
+ // length is in 8-byte words.
+ const f64Words = f64Len / 8;
+ const f64Off = batchBytes.byteOffset + reader.pos;
+ if (f64Off % 8 === 0) {
+ this.float64Cells =
+ new Float64Array(batchBytes.buffer, f64Off, f64Words);
+ } else {
+ // When using the production code in trace_processor's rpc.cc, the
+ // float64 should be 8-bytes aligned. The slow-path case is only for
+ // tests.
+ const slice = batchBytes.buffer.slice(f64Off, f64Off + f64Len);
+ this.float64Cells = new Float64Array(slice);
+ }
+ reader.pos += f64Len;
+ break;
+
+ case 4: // blob_cells: one entry per blob.
+ assertTrue((tag & 7) === TAG_LEN_DELIM);
+ // protobufjs's bytes() under the hoods calls slice() and creates
+ // a copy. Fine here as blobs are rare and not a fastpath.
+ this.blobCells.push(new Uint8Array(reader.bytes()));
+ break;
+
+ case 5: // string_cells: all the string cells concatenated with \0s.
+ assertTrue((tag & 7) === TAG_LEN_DELIM);
+ const strLen = reader.uint32();
+ assertTrue(reader.pos + strLen <= end);
+ const subArr = batchBytes.subarray(reader.pos, reader.pos + strLen);
+ assertTrue(subArr.length === strLen);
+ // The reason why we do this split rather than creating one string
+ // per entry is that utf8 decoding has some non-negligible cost. See
+ // go/postmessage-benchmark .
+ this.stringCells = utf8Decode(subArr).split('\0');
+ reader.pos += strLen;
+ break;
+
+ case 6: // is_last_batch (boolean).
+ this.isLastBatch = !!reader.bool();
+ break;
+
+ case 7: // padding for realignment, skip silently.
+ reader.skipType(tag & 7);
+ break;
+
+ default:
+ console.warn(`Unexpected QueryResult.CellsBatch field ${tag >>> 3}`);
+ reader.skipType(tag & 7);
+ break;
+ } // switch(tag)
+ } // while (pos < end)
+ }
+
+ get numCells() {
+ return this.cellTypesLen;
+ }
+}
+
+class RowIteratorImpl implements RowIteratorBase {
+ // The spec passed to the iter call containing the expected types, e.g.:
+ // {'colA': NUM, 'colB': NUM_NULL, 'colC': STRING}.
+ // This doesn't ever change.
+ readonly rowSpec: Row;
+
+ // The object that holds the current row. This points to the parent
+ // RowIteratorImplWithRowData instance that created this class.
+ rowData: Row;
+
+ // The QueryResult object we are reading data from. The engine will pump
+ // batches over time into this object.
+ private resultObj: QueryResultImpl;
+
+ // All the member variables in the group below point to the identically-named
+ // members in result.batch[batchIdx]. This is to avoid indirection layers in
+ // the next() hotpath, so we can do this.float64Cells vs
+ // this.resultObj.batch[this.batchIdx].float64Cells.
+ // These are re-set every time tryMoveToNextBatch() is called (and succeeds).
+ private batchIdx = -1; // The batch index within |result.batches[]|.
+ private batchBytes = new Uint8Array();
+ private columnNames: string[] = [];
+ private numColumns = 0;
+ private cellTypesEnd = -1; // -1 so the 1st next() hits tryMoveToNextBatch().
+ private float64Cells = new Float64Array();
+ private varIntReader = protobuf.Reader.create(this.batchBytes);
+ private blobCells: Uint8Array[] = [];
+ private stringCells: string[] = [];
+
+ // These members instead are incremented as we read cells from next(). They
+ // are the mutable state of the iterator.
+ private nextCellTypeOff = 0;
+ private nextFloat64Cell = 0;
+ private nextStringCell = 0;
+ private nextBlobCell = 0;
+ private isValid = false;
+
+ constructor(querySpec: Row, rowData: Row, res: QueryResultImpl) {
+ Object.assign(this, querySpec);
+ this.rowData = rowData;
+ this.rowSpec = {...querySpec}; // ... -> Copy all the key/value pairs.
+ this.resultObj = res;
+ this.next();
+ }
+
+ valid(): boolean {
+ return this.isValid;
+ }
+
+
+ get(columnName: string): ColumnType {
+ const res = this.rowData[columnName];
+ if (res === undefined) {
+ throw new Error(
+ `Column '${columnName}' doesn't exist. ` +
+ `Actual columns: [${this.columnNames.join(',')}]`);
+ }
+ return res;
+ }
+
+ // Moves the cursor next by one row and updates |isValid|.
+ // When this fails to move, two cases are possible:
+ // 1. We reached the end of the result set (this is the case if
+ // QueryResult.isComplete() == true when this fails).
+ // 2. We reached the end of the current batch, but more rows might come later
+ // (if QueryResult.isComplete() == false).
+ next() {
+ // At some point we might reach the end of the current batch, but the next
+ // batch might be available already. In this case we want next() to
+ // transparently move on to the next batch.
+ while (this.nextCellTypeOff + this.numColumns > this.cellTypesEnd) {
+ // If TraceProcessor is behaving well, we should never end up in a
+ // situation where we have leftover cells. TP is expected to serialize
+ // whole rows in each QueryResult batch and NOT truncate them midway.
+ // If this assert fires the TP RPC logic has a bug.
+ assertTrue(
+ this.nextCellTypeOff === this.cellTypesEnd ||
+ this.cellTypesEnd === -1);
+ if (!this.tryMoveToNextBatch()) {
+ this.isValid = false;
+ return;
+ }
+ }
+
+ const rowData = this.rowData;
+ const numColumns = this.numColumns;
+
+ // Read the current row.
+ for (let i = 0; i < numColumns; i++) {
+ const cellType = this.batchBytes[this.nextCellTypeOff++];
+ const colName = this.columnNames[i];
+ const expType = this.rowSpec[colName];
+
+ switch (cellType) {
+ case CellType.CELL_NULL:
+ rowData[colName] = null;
+ break;
+
+ case CellType.CELL_VARINT:
+ if (expType === NUM || expType === NUM_NULL) {
+ // This is very subtle. The return type of int64 can be either a
+ // number or a Long.js {high:number, low:number} if Long.js is
+ // installed. The default state seems different in node and browser.
+ // We force-disable Long.js support in the top of this source file.
+ const val = this.varIntReader.int64();
+ rowData[colName] = val as {} as number;
+ } else {
+ // LONG, LONG_NULL, or unspecified - return as bigint
+ const value =
+ decodeInt64Varint(this.batchBytes, this.varIntReader.pos);
+ rowData[colName] = value;
+ this.varIntReader.skip(); // Skips a varint
+ }
+ break;
+
+ case CellType.CELL_FLOAT64:
+ rowData[colName] = this.float64Cells[this.nextFloat64Cell++];
+ break;
+
+ case CellType.CELL_STRING:
+ rowData[colName] = this.stringCells[this.nextStringCell++];
+ break;
+
+ case CellType.CELL_BLOB:
+ const blob = this.blobCells[this.nextBlobCell++];
+ rowData[colName] = blob;
+ break;
+
+ default:
+ throw new Error(`Invalid cell type ${cellType}`);
+ }
+ } // For (cells)
+ this.isValid = true;
+ }
+
+ private tryMoveToNextBatch(): boolean {
+ const nextBatchIdx = this.batchIdx + 1;
+ if (nextBatchIdx >= this.resultObj.batches.length) {
+ return false;
+ }
+
+ this.columnNames = this.resultObj.columnNames;
+ this.numColumns = this.columnNames.length;
+
+ this.batchIdx = nextBatchIdx;
+ const batch = assertExists(this.resultObj.batches[nextBatchIdx]);
+ this.batchBytes = batch.batchBytes;
+ this.nextCellTypeOff = batch.cellTypesOff;
+ this.cellTypesEnd = batch.cellTypesOff + batch.cellTypesLen;
+ this.float64Cells = batch.float64Cells;
+ this.blobCells = batch.blobCells;
+ this.stringCells = batch.stringCells;
+ this.varIntReader = protobuf.Reader.create(batch.batchBytes);
+ this.varIntReader.pos = batch.varintOff;
+ this.varIntReader.len = batch.varintOff + batch.varintLen;
+ this.nextFloat64Cell = 0;
+ this.nextStringCell = 0;
+ this.nextBlobCell = 0;
+
+ // Check that all the expected columns are present.
+ for (const expectedCol of Object.keys(this.rowSpec)) {
+ if (this.columnNames.indexOf(expectedCol) < 0) {
+ throw new Error(
+ `Column ${expectedCol} not found in the SQL result ` +
+ `set {${this.columnNames.join(' ')}}`);
+ }
+ }
+
+ // Check that the cells types are consistent.
+ const numColumns = this.numColumns;
+ if (batch.numCells === 0) {
+ // This can happen if the query result contains just an error. In this
+ // an empty batch with isLastBatch=true is appended as an EOF marker.
+ // In theory TraceProcessor could return an empty batch in the middle and
+ // that would be fine from a protocol viewpoint. In practice, no code path
+ // does that today so it doesn't make sense trying supporting it with a
+ // recursive call to tryMoveToNextBatch().
+ assertTrue(batch.isLastBatch);
+ return false;
+ }
+
+ assertTrue(numColumns > 0);
+ for (let i = this.nextCellTypeOff; i < this.cellTypesEnd; i++) {
+ const col = (i - this.nextCellTypeOff) % numColumns;
+ const colName = this.columnNames[col];
+ const actualType = this.batchBytes[i] as CellType;
+ const expType = this.rowSpec[colName];
+
+ // If undefined, the caller doesn't want to read this column at all, so
+ // it can be whatever.
+ if (expType === undefined) continue;
+
+ let err = '';
+ if (!isCompatible(actualType, expType)) {
+ if (actualType === CellType.CELL_NULL) {
+ err = 'SQL value is NULL but that was not expected' +
+ ` (expected type: ${columnTypeToString(expType)}). ` +
+ 'Did you mean NUM_NULL, LONG_NULL, STR_NULL or BLOB_NULL?';
+ } else {
+ err = `Incompatible cell type. Expected: ${
+ columnTypeToString(
+ expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+ }
+ }
+ if (err.length > 0) {
+ throw new Error(
+ `Error @ row: ${Math.floor(i / numColumns)} col: '` +
+ `${colName}': ${err}`);
+ }
+ }
+ return true;
+ }
+}
+
+// This is the object ultimately returned to the client when calling
+// QueryResult.iter(...).
+// The only reason why this is disjoint from RowIteratorImpl is to avoid
+// naming collisions between the members variables required by RowIteratorImpl
+// and the column names returned by the iterator.
+class RowIteratorImplWithRowData implements RowIteratorBase {
+ private _impl: RowIteratorImpl;
+
+ next: () => void;
+ valid: () => boolean;
+ get: (columnName: string) => ColumnType;
+
+ constructor(querySpec: Row, res: QueryResultImpl) {
+ const thisAsRow = this as {} as Row;
+ Object.assign(thisAsRow, querySpec);
+ this._impl = new RowIteratorImpl(querySpec, thisAsRow, res);
+ this.next = this._impl.next.bind(this._impl);
+ this.valid = this._impl.valid.bind(this._impl);
+ this.get = this._impl.get.bind(this._impl);
+ }
+}
+
+// This is a proxy object that wraps QueryResultImpl, adding await-ability.
+// This is so that:
+// 1. Clients that just want to await for the full result set can just call
+// await engine.query('...') and will get a QueryResult that is guaranteed
+// to be complete.
+// 2. Clients that know how to handle the streaming can use it straight away.
+class WaitableQueryResultImpl implements QueryResult, WritableQueryResult,
+ PromiseLike<QueryResult> {
+ private impl: QueryResultImpl;
+ private thenCalled = false;
+
+ constructor(errorInfo: QueryErrorInfo) {
+ this.impl = new QueryResultImpl(errorInfo);
+ }
+
+ // QueryResult implementation. Proxies all calls to the impl object.
+ iter<T extends Row>(spec: T) {
+ return this.impl.iter(spec);
+ }
+ firstRow<T extends Row>(spec: T) {
+ return this.impl.firstRow(spec);
+ }
+ waitAllRows() {
+ return this.impl.waitAllRows();
+ }
+ waitMoreRows() {
+ return this.impl.waitMoreRows();
+ }
+ isComplete() {
+ return this.impl.isComplete();
+ }
+ numRows() {
+ return this.impl.numRows();
+ }
+ columns() {
+ return this.impl.columns();
+ }
+ error() {
+ return this.impl.error();
+ }
+ statementCount() {
+ return this.impl.statementCount();
+ }
+ statementWithOutputCount() {
+ return this.impl.statementWithOutputCount();
+ }
+ lastStatementSql() {
+ return this.impl.lastStatementSql();
+ }
+
+ // WritableQueryResult implementation.
+ appendResultBatch(resBytes: Uint8Array) {
+ return this.impl.appendResultBatch(resBytes);
+ }
+
+ // PromiseLike<QueryResult> implementaton.
+
+ then(onfulfilled: any, onrejected: any): any {
+ assertFalse(this.thenCalled);
+ this.thenCalled = true;
+ return this.impl.ensureAllRowsPromise().then(onfulfilled, onrejected);
+ }
+
+ catch(error: any): any {
+ return this.impl.ensureAllRowsPromise().catch(error);
+ }
+
+ finally(callback: () => void): any {
+ return this.impl.ensureAllRowsPromise().finally(callback);
+ }
+
+ // eslint and clang-format disagree on how to format get[foo](). Let
+ // clang-format win:
+ // eslint-disable-next-line keyword-spacing
+ get[Symbol.toStringTag](): string {
+ return 'Promise<WaitableQueryResult>';
+ }
+}
+
+export function createQueryResult(errorInfo: QueryErrorInfo): QueryResult&
+ Promise<QueryResult>&WritableQueryResult {
+ return new WaitableQueryResultImpl(errorInfo);
+}
diff --git a/tools/winscope/src/trace_processor/string_utils.ts b/tools/winscope/src/trace_processor/string_utils.ts
new file mode 100644
index 0000000..bc33b70
--- /dev/null
+++ b/tools/winscope/src/trace_processor/string_utils.ts
@@ -0,0 +1,117 @@
+// Copyright (C) 2019 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.
+
+import {
+ decode as b64Decode,
+ encode as b64Encode,
+ length as b64Len,
+} from '@protobufjs/base64';
+import {
+ length as utf8Len,
+ read as utf8Read,
+ write as utf8Write,
+} from '@protobufjs/utf8';
+
+import {assertTrue} from './logging';
+
+// TextDecoder/Decoder requires the full DOM and isn't available in all types
+// of tests. Use fallback implementation from protbufjs.
+let Utf8Decoder: {decode: (buf: Uint8Array) => string;};
+let Utf8Encoder: {encode: (str: string) => Uint8Array;};
+try {
+ Utf8Decoder = new TextDecoder('utf-8');
+ Utf8Encoder = new TextEncoder();
+} catch (_) {
+ if (typeof process === 'undefined') {
+ // Silence the warning when we know we are running under NodeJS.
+ console.warn(
+ 'Using fallback UTF8 Encoder/Decoder, This should happen only in ' +
+ 'tests and NodeJS-based environments, not in browsers.');
+ }
+ Utf8Decoder = {decode: (buf: Uint8Array) => utf8Read(buf, 0, buf.length)};
+ Utf8Encoder = {
+ encode: (str: string) => {
+ const arr = new Uint8Array(utf8Len(str));
+ const written = utf8Write(str, arr, 0);
+ assertTrue(written === arr.length);
+ return arr;
+ },
+ };
+}
+
+export function base64Encode(buffer: Uint8Array): string {
+ return b64Encode(buffer, 0, buffer.length);
+}
+
+export function base64Decode(str: string): Uint8Array {
+ // if the string is in base64url format, convert to base64
+ const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
+ const arr = new Uint8Array(b64Len(b64));
+ const written = b64Decode(b64, arr, 0);
+ assertTrue(written === arr.length);
+ return arr;
+}
+
+// encode binary array to hex string
+export function hexEncode(bytes: Uint8Array): string {
+ return bytes.reduce(
+ (prev, cur) => prev + ('0' + cur.toString(16)).slice(-2), '');
+}
+
+export function utf8Encode(str: string): Uint8Array {
+ return Utf8Encoder.encode(str);
+}
+
+// Note: not all byte sequences can be converted to<>from UTF8. This can be
+// used only with valid unicode strings, not arbitrary byte buffers.
+export function utf8Decode(buffer: Uint8Array): string {
+ return Utf8Decoder.decode(buffer);
+}
+
+// The binaryEncode/Decode functions below allow to encode an arbitrary binary
+// buffer into a string that can be JSON-encoded. binaryEncode() applies
+// UTF-16 encoding to each byte individually.
+// Unlike utf8Encode/Decode, any arbitrary byte sequence can be converted into a
+// valid string, and viceversa.
+// This should be only used when a byte array needs to be transmitted over an
+// interface that supports only JSON serialization (e.g., postmessage to a
+// chrome extension).
+
+export function binaryEncode(buf: Uint8Array): string {
+ let str = '';
+ for (let i = 0; i < buf.length; i++) {
+ str += String.fromCharCode(buf[i]);
+ }
+ return str;
+}
+
+export function binaryDecode(str: string): Uint8Array {
+ const buf = new Uint8Array(str.length);
+ const strLen = str.length;
+ for (let i = 0; i < strLen; i++) {
+ buf[i] = str.charCodeAt(i);
+ }
+ return buf;
+}
+
+// A function used to interpolate strings into SQL query. The only replacement
+// is done is that single quote replaced with two single quotes, according to
+// SQLite documentation:
+// https://www.sqlite.org/lang_expr.html#literal_values_constants_
+//
+// The purpose of this function is to use in simple comparisons, to escape
+// strings used in GLOB clauses see escapeQuery function.
+export function sqliteString(str: string): string {
+ return `'${str.replace(/'/g, '\'\'')}'`;
+}
diff --git a/tools/winscope/src/trace_processor/wasm_engine_proxy.ts b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
new file mode 100644
index 0000000..90add37
--- /dev/null
+++ b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
@@ -0,0 +1,74 @@
+// Copyright (C) 2018 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.
+
+import {assertExists, assertTrue} from './logging';
+
+import {Engine, LoadingTracker} from './engine';
+import {EngineWorkerInitMessage} from './worker_messages';
+
+let bundlePath: string;
+let idleWasmWorker: Worker;
+let activeWasmWorker: Worker;
+
+export function initWasm(root: string) {
+ bundlePath = root + 'engine_bundle.js';
+ idleWasmWorker = new Worker(bundlePath);
+}
+
+// This method is called trace_controller whenever a new trace is loaded.
+export function resetEngineWorker(): MessagePort {
+ const channel = new MessageChannel();
+ const port = channel.port1;
+
+ // We keep always an idle worker around, the first one is created by the
+ // main() below, so we can hide the latency of the Wasm initialization.
+ if (activeWasmWorker !== undefined) {
+ activeWasmWorker.terminate();
+ }
+
+ // Swap the active worker with the idle one and create a new idle worker
+ // for the next trace.
+ activeWasmWorker = assertExists(idleWasmWorker);
+ activeWasmWorker.postMessage(port, [port]);
+ idleWasmWorker = new Worker(bundlePath);
+ return channel.port2;
+}
+
+/**
+ * This implementation of Engine uses a WASM backend hosted in a separate
+ * worker thread.
+ */
+export class WasmEngineProxy extends Engine {
+ readonly id: string;
+ private port: MessagePort;
+
+ constructor(id: string, port: MessagePort, loadingTracker?: LoadingTracker) {
+ super(loadingTracker);
+ this.id = id;
+ this.port = port;
+ this.port.onmessage = this.onMessage.bind(this);
+ }
+
+ onMessage(m: MessageEvent) {
+ assertTrue(m.data instanceof Uint8Array);
+ super.onRpcResponseBytes(m.data as Uint8Array);
+ }
+
+ rpcSendRequestBytes(data: Uint8Array): void {
+ // We deliberately don't use a transfer list because protobufjs reuses the
+ // same buffer when encoding messages (which is good, because creating a new
+ // TypedArray for each decode operation would be too expensive).
+ this.port.postMessage(data);
+ }
+}
diff --git a/tools/winscope/src/trace_processor/worker_messages.ts b/tools/winscope/src/trace_processor/worker_messages.ts
new file mode 100644
index 0000000..166b1cc
--- /dev/null
+++ b/tools/winscope/src/trace_processor/worker_messages.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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.
+
+// This file defines the API of messages exchanged between frontend and
+// {engine, controller} worker when bootstrapping the workers.
+// Those messages are sent only once. The rest of the communication happens
+// over the MessagePort(s) that are sent in the init message.
+
+// This is so we can create all the workers in a central place in the frontend
+// (Safari still doesn't spawning workers from other workers) but then let them
+// communicate by sending the right MessagePort to them.
+
+// Frontend -> Engine initialization message.
+export interface EngineWorkerInitMessage {
+ // The port used to receive engine messages (e.g., query commands).
+ // The controller owns the other end of the MessageChannel
+ // (see resetEngineWorker()).
+ enginePort: MessagePort;
+}
diff --git a/tools/winscope/src/viewers/common/ime_ui_data.ts b/tools/winscope/src/viewers/common/ime_ui_data.ts
index 4b8d918..958dea4 100644
--- a/tools/winscope/src/viewers/common/ime_ui_data.ts
+++ b/tools/winscope/src/viewers/common/ime_ui_data.ts
@@ -21,7 +21,7 @@
export class ImeUiData {
dependencies: TraceType[];
- highlightedItems: string[] = [];
+ highlightedItem: string = '';
pinnedItems: HierarchyTreeNode[] = [];
hierarchyUserOptions: UserOptions = {};
propertiesUserOptions: UserOptions = {};
diff --git a/tools/winscope/src/viewers/common/ime_utils.ts b/tools/winscope/src/viewers/common/ime_utils.ts
index 027da4d..e29aee9 100644
--- a/tools/winscope/src/viewers/common/ime_utils.ts
+++ b/tools/winscope/src/viewers/common/ime_utils.ts
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import {FilterType, TreeUtils} from 'common/tree_utils';
-import {WindowContainer} from 'trace/flickerlib/common';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {Activity} from 'trace/flickerlib/windows/Activity';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
-import {WindowState} from 'trace/flickerlib/windows/WindowState';
+import {WindowContainer} from 'flickerlib/common';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {Activity} from 'flickerlib/windows/Activity';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {WindowState} from 'flickerlib/windows/WindowState';
class ProcessedWindowManagerState {
constructor(
diff --git a/tools/winscope/src/viewers/common/presenter_input_method.ts b/tools/winscope/src/viewers/common/presenter_input_method.ts
index ce75695..65ac2aa 100644
--- a/tools/winscope/src/viewers/common/presenter_input_method.ts
+++ b/tools/winscope/src/viewers/common/presenter_input_method.ts
@@ -16,8 +16,9 @@
import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {FilterType, TreeUtils} from 'common/tree_utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace, TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
@@ -48,7 +49,7 @@
readonly notifyViewCallback: NotifyImeViewCallbackType;
protected readonly dependencies: TraceType[];
protected uiData: ImeUiData;
- protected highlightedItems: string[] = [];
+ protected highlightedItem: string = '';
protected entry: TraceTreeNode | null = null;
protected additionalPropertyEntry: TraceTreeNode | null = null;
protected hierarchyUserOptions: UserOptions = PersistentStoreProxy.new<UserOptions>(
@@ -101,24 +102,26 @@
this.notifyViewCallback(this.uiData);
}
- async onTracePositionUpdate(position: TracePosition) {
- this.uiData = new ImeUiData(this.dependencies);
- this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
- this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ this.uiData = new ImeUiData(this.dependencies);
+ this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+ this.uiData.propertiesUserOptions = this.propertiesUserOptions;
- const [imeEntry, sfEntry, wmEntry] = this.findTraceEntries(position);
+ const [imeEntry, sfEntry, wmEntry] = this.findTraceEntries(event.position);
- if (imeEntry) {
- this.entry = (await imeEntry.getValue()) as TraceTreeNode;
- this.uiData.highlightedItems = this.highlightedItems;
- this.uiData.additionalProperties = this.getAdditionalProperties(
- await wmEntry?.getValue(),
- await sfEntry?.getValue()
- );
- this.uiData.tree = this.generateTree();
- this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties();
- }
- this.notifyViewCallback(this.uiData);
+ if (imeEntry) {
+ this.entry = (await imeEntry.getValue()) as TraceTreeNode;
+ this.uiData.highlightedItem = this.highlightedItem;
+ this.uiData.additionalProperties = this.getAdditionalProperties(
+ await wmEntry?.getValue(),
+ await sfEntry?.getValue()
+ );
+ this.uiData.tree = this.generateTree();
+ this.uiData.hierarchyTableProperties = this.updateHierarchyTableProperties();
+ }
+ this.notifyViewCallback(this.uiData);
+ });
}
updatePinnedItems(pinnedItem: HierarchyTreeNode) {
@@ -133,14 +136,13 @@
this.notifyViewCallback(this.uiData);
}
- updateHighlightedItems(id: string) {
- if (this.highlightedItems.includes(id)) {
- this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
+ updateHighlightedItem(id: string) {
+ if (this.highlightedItem === id) {
+ this.highlightedItem = '';
} else {
- this.highlightedItems = []; //if multi-select surfaces implemented, remove this line
- this.highlightedItems.push(id);
+ this.highlightedItem = id; //if multi-select surfaces implemented, remove this line
}
- this.uiData.highlightedItems = this.highlightedItems;
+ this.uiData.highlightedItem = this.highlightedItem;
this.notifyViewCallback(this.uiData);
}
diff --git a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
index 62a56ca..ce1f292 100644
--- a/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
+++ b/tools/winscope/src/viewers/common/presenter_input_method_test_utils.ts
@@ -15,13 +15,13 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
import {MockStorage} from 'test/unit/mock_storage';
import {TracesBuilder} from 'test/unit/traces_builder';
import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {ImeUiData} from 'viewers/common/ime_ui_data';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -45,7 +45,7 @@
describe('PresenterInputMethod', () => {
let presenter: PresenterInputMethod;
let uiData: ImeUiData;
- let position: TracePosition;
+ let positionUpdate: TracePositionUpdate;
let selectedTree: HierarchyTreeNode;
beforeEach(async () => {
@@ -65,7 +65,7 @@
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
@@ -73,7 +73,7 @@
it('is robust to traces without SF', async () => {
await setUpTestEnvironment([imeTraceType, TraceType.WINDOW_MANAGER]);
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -81,7 +81,7 @@
it('is robust to traces without WM', async () => {
await setUpTestEnvironment([imeTraceType, TraceType.SURFACE_FLINGER]);
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -89,14 +89,14 @@
it('is robust to traces without WM and SF', async () => {
await setUpTestEnvironment([imeTraceType]);
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
});
it('processes trace position updates', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.propertiesUserOptions).toBeTruthy();
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
@@ -113,11 +113,11 @@
expect(uiData.pinnedItems).toContain(pinnedItem);
});
- it('can update highlighted items', () => {
- expect(uiData.highlightedItems).toEqual([]);
+ it('can update highlighted item', () => {
+ expect(uiData.highlightedItem).toEqual('');
const id = 'entry';
- presenter.updateHighlightedItems(id);
- expect(uiData.highlightedItems).toContain(id);
+ presenter.updateHighlightedItem(id);
+ expect(uiData.highlightedItem).toBe(id);
});
it('can update hierarchy tree', async () => {
@@ -138,7 +138,7 @@
};
let expectedChildren = expectHierarchyTreeWithSfSubtree ? 2 : 1;
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.tree?.children.length).toEqual(expectedChildren);
// Filter out non-visible child
@@ -165,7 +165,7 @@
};
const expectedChildren = expectHierarchyTreeWithSfSubtree ? 12 : 1;
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.updateHierarchyTree(userOptions);
expect(uiData.tree?.children.length).toEqual(expectedChildren);
@@ -175,14 +175,14 @@
});
it('can set new properties tree and associated ui data', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
// does not check specific tree values as tree transformation method may change
expect(uiData.propertiesTree).toBeTruthy();
});
it('can filter properties tree', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
let nonTerminalChildren =
uiData.propertiesTree?.children?.filter(
@@ -199,7 +199,7 @@
expect(nonTerminalChildren.length).toEqual(expectedChildren[1]);
});
- const setUpTestEnvironment = async (traceTypes: TraceType[]) => {
+ async function setUpTestEnvironment(traceTypes: TraceType[]) {
const traces = new Traces();
const entries = await UnitTestUtils.getImeTraceEntries();
@@ -213,12 +213,11 @@
presenter = createPresenter(traces);
- position = TracePosition.fromTraceEntry(
- assertDefined(traces.getTrace(imeTraceType)).getEntry(0)
- );
- };
+ const entry = assertDefined(traces.getTrace(imeTraceType)).getEntry(0);
+ positionUpdate = TracePositionUpdate.fromTraceEntry(entry);
+ }
- const createPresenter = (traces: Traces): PresenterInputMethod => {
+ function createPresenter(traces: Traces): PresenterInputMethod {
return new PresenterInputMethod(
traces,
new MockStorage(),
@@ -227,6 +226,6 @@
uiData = newData;
}
);
- };
+ }
});
}
diff --git a/tools/winscope/src/viewers/common/surface_flinger_utils.ts b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
new file mode 100644
index 0000000..1bca894
--- /dev/null
+++ b/tools/winscope/src/viewers/common/surface_flinger_utils.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {TransformMatrix} from 'common/geometry_utils';
+import {Layer, LayerTraceEntry} from 'flickerlib/common';
+import {UiRect} from 'viewers/components/rects/types2d';
+import {UserOptions} from './user_options';
+
+export class SurfaceFlingerUtils {
+ static makeRects(
+ entry: LayerTraceEntry,
+ viewCapturePackageNames: string[],
+ hierarchyUserOptions: UserOptions
+ ): UiRect[] {
+ const layerRects = SurfaceFlingerUtils.makeLayerRects(
+ entry,
+ viewCapturePackageNames,
+ hierarchyUserOptions
+ );
+ const displayRects = SurfaceFlingerUtils.makeDisplayRects(entry);
+ return layerRects.concat(displayRects);
+ }
+
+ private static makeLayerRects(
+ entry: LayerTraceEntry,
+ viewCapturePackageNames: string[],
+ hierarchyUserOptions: UserOptions
+ ): UiRect[] {
+ return entry.flattenedLayers
+ .filter((layer: Layer) => {
+ return SurfaceFlingerUtils.isLayerToRenderInRectsComponent(layer, hierarchyUserOptions);
+ })
+ .sort(SurfaceFlingerUtils.compareLayerZ)
+ .map((it: Layer) => {
+ const transform: TransformMatrix = it.rect.transform?.matrix ?? it.rect.transform;
+ const rect: UiRect = {
+ x: it.rect.left,
+ y: it.rect.top,
+ w: it.rect.right - it.rect.left,
+ h: it.rect.bottom - it.rect.top,
+ label: it.rect.label,
+ transform,
+ isVisible: it.isVisible,
+ isDisplay: false,
+ id: it.stableId,
+ displayId: it.stackId,
+ isVirtual: false,
+ isClickable: true,
+ cornerRadius: it.cornerRadius,
+ hasContent: viewCapturePackageNames.includes(
+ it.rect.label.substring(0, it.rect.label.indexOf('/'))
+ ),
+ };
+ return rect;
+ });
+ }
+
+ private static makeDisplayRects(entry: LayerTraceEntry): UiRect[] {
+ if (!entry.displays) {
+ return [];
+ }
+
+ return entry.displays?.map((display: any) => {
+ const transform: TransformMatrix = display.transform?.matrix ?? display.transform;
+ const rect: UiRect = {
+ x: 0,
+ y: 0,
+ w: display.size.width,
+ h: display.size.height,
+ label: 'Display',
+ transform,
+ isVisible: false,
+ isDisplay: true,
+ id: `Display - ${display.id}`,
+ displayId: display.layerStackId,
+ isVirtual: display.isVirtual ?? false,
+ isClickable: false,
+ cornerRadius: 0,
+ hasContent: false,
+ };
+ return rect;
+ });
+ }
+
+ private static isLayerToRenderInRectsComponent(
+ layer: Layer,
+ hierarchyUserOptions: UserOptions
+ ): boolean {
+ const onlyVisible = hierarchyUserOptions['onlyVisible']?.enabled ?? false;
+ // Show only visible layers or Visible + Occluded layers. Don't show all layers
+ // (flattenedLayers) because container layers are never meant to be displayed
+ return layer.isVisible || (!onlyVisible && layer.occludedBy.length > 0);
+ }
+
+ static compareLayerZ(a: Layer, b: Layer): number {
+ const zipLength = Math.min(a.zOrderPath.length, b.zOrderPath.length);
+ for (let i = 0; i < zipLength; ++i) {
+ const zOrderA = a.zOrderPath[i];
+ const zOrderB = b.zOrderPath[i];
+ if (zOrderA > zOrderB) return -1;
+ if (zOrderA < zOrderB) return 1;
+ }
+ // When z-order is the same, the layer with larger ID is on top
+ return a.id > b.id ? -1 : 1;
+ }
+}
diff --git a/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts b/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts
new file mode 100644
index 0000000..fb604f2
--- /dev/null
+++ b/tools/winscope/src/viewers/common/surface_flinger_utils_test.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {Layer} from 'flickerlib/common';
+import {SurfaceFlingerUtils as Utils} from './surface_flinger_utils';
+
+describe('SurfaceFlingerUtils', () => {
+ describe("Layer's z order comparison", () => {
+ it('handles z-order paths with equal lengths', () => {
+ const a: Layer = {
+ zOrderPath: [1],
+ };
+ const b: Layer = {
+ zOrderPath: [0],
+ };
+ expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+ expect(Utils.compareLayerZ(b, a)).toEqual(1);
+ });
+
+ it('handles z-order paths with different lengths', () => {
+ const a: Layer = {
+ zOrderPath: [0, 1],
+ };
+ const b: Layer = {
+ zOrderPath: [0, 0, 0],
+ };
+ expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+ expect(Utils.compareLayerZ(b, a)).toEqual(1);
+ });
+
+ it('handles z-order paths with equal values (fall back to Layer ID comparison)', () => {
+ const a: Layer = {
+ id: 1,
+ zOrderPath: [0, 1],
+ };
+ const b: Layer = {
+ id: 0,
+ zOrderPath: [0, 1, 0],
+ };
+ expect(Utils.compareLayerZ(a, b)).toEqual(-1);
+ expect(Utils.compareLayerZ(b, a)).toEqual(1);
+ });
+ });
+});
diff --git a/tools/winscope/src/viewers/common/tree_generator.ts b/tools/winscope/src/viewers/common/tree_generator.ts
index 51f9207..e697dab 100644
--- a/tools/winscope/src/viewers/common/tree_generator.ts
+++ b/tools/winscope/src/viewers/common/tree_generator.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {FilterType, TreeNode} from 'common/tree_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {FilterType, TreeUtilsNode} from 'common/tree_utils';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {
GPU_CHIP,
@@ -155,7 +155,7 @@
}
}
- private filterMatches(item?: TreeNode | null): boolean {
+ private filterMatches(item?: TreeUtilsNode | null): boolean {
return this.filter(item) ?? false;
}
@@ -184,11 +184,16 @@
}
private addChips(tree: HierarchyTreeNode): HierarchyTreeNode {
- if (tree.hwcCompositionType === HwcCompositionType.CLIENT) {
+ if (
+ tree.hwcCompositionType === HwcCompositionType.CLIENT ||
+ tree.hwcCompositionType?.toString() === 'HWC_TYPE_CLIENT'
+ ) {
tree.chips.push(GPU_CHIP);
} else if (
tree.hwcCompositionType === HwcCompositionType.DEVICE ||
- tree.hwcCompositionType === HwcCompositionType.SOLID_COLOR
+ tree.hwcCompositionType?.toString() === 'HWC_TYPE_DEVICE' ||
+ tree.hwcCompositionType === HwcCompositionType.SOLID_COLOR ||
+ tree.hwcCompositionType?.toString() === 'HWC_TYPE_SOLID_COLOR'
) {
tree.chips.push(HWC_CHIP);
}
diff --git a/tools/winscope/src/viewers/common/tree_transformer.ts b/tools/winscope/src/viewers/common/tree_transformer.ts
index a39eb3e..86218bd 100644
--- a/tools/winscope/src/viewers/common/tree_transformer.ts
+++ b/tools/winscope/src/viewers/common/tree_transformer.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {FilterType, TreeNode} from 'common/tree_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {FilterType, TreeUtilsNode} from 'common/tree_utils';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {
@@ -343,7 +343,7 @@
private filterMatches(item: PropertiesDump | null): boolean {
//TODO: fix PropertiesDump type. What is it? Why does it declare only a "key" property and yet it is used as a TreeNode?
- return this.filter(item as TreeNode) ?? false;
+ return this.filter(item as TreeUtilsNode) ?? false;
}
private transformProperties(
diff --git a/tools/winscope/src/viewers/common/ui_tree_utils.ts b/tools/winscope/src/viewers/common/ui_tree_utils.ts
index 0039e71..7b72117 100644
--- a/tools/winscope/src/viewers/common/ui_tree_utils.ts
+++ b/tools/winscope/src/viewers/common/ui_tree_utils.ts
@@ -83,15 +83,19 @@
return diffType ?? '';
}
- static isHighlighted(item: UiTreeNode, highlightedItems: string[]) {
- return item instanceof HierarchyTreeNode && highlightedItems.includes(`${item.stableId}`);
+ static isHighlighted(item: UiTreeNode, highlighted: string): boolean {
+ return highlighted === `${item.stableId}`;
}
- static isVisibleNode(kind: string, type?: string) {
- return kind === 'WindowState' || kind === 'Activity' || type?.includes('Layer');
+ static isVisibleNode(kind: string, type?: string): boolean {
+ return kind === 'WindowState' || kind === 'Activity' || UiTreeUtils.isSFEntryNode(type);
}
- static isParentNode(kind: string) {
+ private static isSFEntryNode(type?: string): boolean {
+ return type === undefined;
+ }
+
+ static isParentNode(kind: string): boolean {
return UiTreeUtils.PARENT_NODE_KINDS.includes(kind);
}
diff --git a/tools/winscope/src/viewers/common/user_options.ts b/tools/winscope/src/viewers/common/user_options.ts
index d5c2bdb..54fffa2 100644
--- a/tools/winscope/src/viewers/common/user_options.ts
+++ b/tools/winscope/src/viewers/common/user_options.ts
@@ -18,5 +18,6 @@
name: string;
enabled: boolean;
tooltip?: string;
+ isUnavailable?: boolean;
};
}
diff --git a/tools/winscope/src/viewers/common/view_capture_utils.ts b/tools/winscope/src/viewers/common/view_capture_utils.ts
new file mode 100644
index 0000000..f0a0487
--- /dev/null
+++ b/tools/winscope/src/viewers/common/view_capture_utils.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import {CustomQueryType} from 'trace/custom_query';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceType} from 'trace/trace_type';
+
+export class ViewCaptureUtils {
+ static readonly NEXUS_LAUNCHER_PACKAGE_NAME = 'com.google.android.apps.nexuslauncher';
+
+ static async getPackageNames(traces: Traces): Promise<string[]> {
+ const packageNames: string[] = [];
+
+ const viewCaptureTraces = [
+ traces.getTrace(TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY),
+ traces.getTrace(TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER),
+ traces.getTrace(TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER),
+ ].filter((trace) => trace !== undefined) as Array<Trace<object>>;
+
+ for (const trace of viewCaptureTraces) {
+ const packageName = await trace.customQuery(CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME);
+ if (packageNames.includes(packageName)) {
+ continue;
+ }
+ packageNames.push(packageName);
+ }
+
+ return packageNames;
+ }
+}
diff --git a/tools/winscope/src/viewers/common/viewer_events.ts b/tools/winscope/src/viewers/common/viewer_events.ts
index fd109da..a938412 100644
--- a/tools/winscope/src/viewers/common/viewer_events.ts
+++ b/tools/winscope/src/viewers/common/viewer_events.ts
@@ -16,10 +16,17 @@
export const ViewerEvents = {
HierarchyPinnedChange: 'HierarchyPinnedChange',
HighlightedChange: 'HighlightedChange',
+ HighlightedPropertyChange: 'HighlightedPropertyChange',
HierarchyUserOptionsChange: 'HierarchyUserOptionsChange',
HierarchyFilterChange: 'HierarchyFilterChange',
SelectedTreeChange: 'SelectedTreeChange',
PropertiesUserOptionsChange: 'PropertiesUserOptionsChange',
PropertiesFilterChange: 'PropertiesFilterChange',
AdditionalPropertySelected: 'AdditionalPropertySelected',
+ RectsDblClick: 'RectsDblClick',
+ MiniRectsDblClick: 'MiniRectsDblClick',
};
+
+export class RectDblClickDetail {
+ constructor(public clickedRectId: string) {}
+}
diff --git a/tools/winscope/src/viewers/common/viewer_input_method.ts b/tools/winscope/src/viewers/common/viewer_input_method.ts
index 61c3e31..8290890 100644
--- a/tools/winscope/src/viewers/common/viewer_input_method.ts
+++ b/tools/winscope/src/viewers/common/viewer_input_method.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {WinscopeEvent} from 'messaging/winscope_event';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {ImeUiData} from 'viewers/common/ime_ui_data';
import {PresenterInputMethod} from 'viewers/common/presenter_input_method';
@@ -23,14 +23,21 @@
import {View, Viewer} from 'viewers/viewer';
abstract class ViewerInputMethod implements Viewer {
+ protected htmlElement: HTMLElement;
+ protected presenter: PresenterInputMethod;
+
constructor(traces: Traces, storage: Storage) {
this.htmlElement = document.createElement('viewer-input-method');
this.presenter = this.initialisePresenter(traces, storage);
this.addViewerEventListeners();
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent() {
+ // do nothing
}
abstract getViews(): View[];
@@ -49,7 +56,7 @@
this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem)
);
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) =>
- this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)
+ this.presenter.updateHighlightedItem(`${(event as CustomEvent).detail.id}`)
);
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) =>
this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)
@@ -72,9 +79,6 @@
}
protected abstract initialisePresenter(traces: Traces, storage: Storage): PresenterInputMethod;
-
- protected htmlElement: HTMLElement;
- protected presenter: PresenterInputMethod;
}
export {ViewerInputMethod};
diff --git a/tools/winscope/src/viewers/components/hierarchy_component.ts b/tools/winscope/src/viewers/components/hierarchy_component.ts
index dd4ca94..d902200 100644
--- a/tools/winscope/src/viewers/components/hierarchy_component.ts
+++ b/tools/winscope/src/viewers/components/hierarchy_component.ts
@@ -28,7 +28,7 @@
<div class="view-header">
<div class="title-filter">
<h2 class="hierarchy-title mat-title">Hierarchy</h2>
- <mat-form-field>
+ <mat-form-field (keydown.enter)="$event.target.blur()">
<mat-label>Filter...</mat-label>
<input matInput [(ngModel)]="filterString" (ngModelChange)="filterTree()" name="filter" />
</mat-form-field>
@@ -38,6 +38,7 @@
*ngFor="let option of objectKeys(userOptions)"
color="primary"
[(ngModel)]="userOptions[option].enabled"
+ [disabled]="userOptions[option].isUnavailable ?? false"
(ngModelChange)="updateTree()"
>{{ userOptions[option].name }}</mat-checkbox
>
@@ -51,12 +52,13 @@
*ngFor="let pinnedItem of pinnedItems"
class="node"
[class]="diffClass(pinnedItem)"
- [class.selected]="isHighlighted(pinnedItem, highlightedItems)"
+ [class.selected]="isHighlighted(pinnedItem, highlightedItem)"
[class.clickable]="true"
[item]="pinnedItem"
[isPinned]="true"
[isInPinnedSection]="true"
- (pinNodeChange)="pinnedItemChange($event)"
+ [isSelected]="isHighlighted(pinnedItem, highlightedItem)"
+ (pinNodeChange)="onPinnedItemChange($event)"
(click)="onPinnedNodeClick($event, pinnedItem)"></tree-node>
</div>
</div>
@@ -70,11 +72,11 @@
[store]="store"
[useGlobalCollapsedState]="true"
[itemsClickable]="true"
- [highlightedItems]="highlightedItems"
+ [highlightedItem]="highlightedItem"
[pinnedItems]="pinnedItems"
- (highlightedItemChange)="highlightedItemChange($event)"
- (pinnedItemChange)="pinnedItemChange($event)"
- (selectedTreeChange)="selectedTreeChange($event)"></tree-view>
+ (highlightedChange)="onHighlightedItemChange($event)"
+ (pinnedItemChange)="onPinnedItemChange($event)"
+ (selectedTreeChange)="onSelectedTreeChange($event)"></tree-view>
</div>
`,
styles: [
@@ -111,7 +113,7 @@
.pinned-items {
width: 100%;
box-sizing: border-box;
- border: 2px solid yellow;
+ border: 2px solid #ffd58b;
}
tree-view {
@@ -130,7 +132,7 @@
@Input() tree!: HierarchyTreeNode | null;
@Input() tableProperties?: TableProperties | null;
@Input() dependencies: TraceType[] = [];
- @Input() highlightedItems: string[] = [];
+ @Input() highlightedItem: string = '';
@Input() pinnedItems: HierarchyTreeNode[] = [];
@Input() store!: PersistentStore;
@Input() userOptions: UserOptions = {};
@@ -146,8 +148,8 @@
if (window.getSelection()?.type === 'range') {
return;
}
- if (pinnedItem.id) this.highlightedItemChange(`${pinnedItem.id}`);
- this.selectedTreeChange(pinnedItem);
+ if (pinnedItem.stableId) this.onHighlightedItemChange(`${pinnedItem.stableId}`);
+ this.onSelectedTreeChange(pinnedItem);
}
updateTree() {
@@ -166,7 +168,7 @@
this.elementRef.nativeElement.dispatchEvent(event);
}
- highlightedItemChange(newId: string) {
+ onHighlightedItemChange(newId: string) {
const event: CustomEvent = new CustomEvent(ViewerEvents.HighlightedChange, {
bubbles: true,
detail: {id: newId},
@@ -174,7 +176,7 @@
this.elementRef.nativeElement.dispatchEvent(event);
}
- selectedTreeChange(item: UiTreeNode) {
+ onSelectedTreeChange(item: UiTreeNode) {
if (!(item instanceof HierarchyTreeNode)) {
return;
}
@@ -185,7 +187,7 @@
this.elementRef.nativeElement.dispatchEvent(event);
}
- pinnedItemChange(item: UiTreeNode) {
+ onPinnedItemChange(item: UiTreeNode) {
if (!(item instanceof HierarchyTreeNode)) {
return;
}
diff --git a/tools/winscope/src/viewers/components/hierarchy_component_test.ts b/tools/winscope/src/viewers/components/hierarchy_component_test.ts
index 33b8417..965d513 100644
--- a/tools/winscope/src/viewers/components/hierarchy_component_test.ts
+++ b/tools/winscope/src/viewers/components/hierarchy_component_test.ts
@@ -16,11 +16,13 @@
import {CommonModule} from '@angular/common';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
+import {FormsModule} from '@angular/forms';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {assertDefined} from 'common/assert_utils';
import {PersistentStore} from 'common/persistent_store';
import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
import {TreeComponent} from 'viewers/components/tree_component';
@@ -49,6 +51,7 @@
MatInputModule,
MatFormFieldModule,
BrowserAnimationsModule,
+ FormsModule,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
@@ -58,18 +61,19 @@
htmlElement = fixture.nativeElement;
component.tree = new HierarchyTreeBuilder()
+ .setStableId('RootNode1')
.setName('Root node')
.setChildren([new HierarchyTreeBuilder().setName('Child node').build()])
.build();
component.store = new PersistentStore();
component.userOptions = {
- onlyVisible: {
- name: 'Only visible',
+ showDiff: {
+ name: 'Show diff',
enabled: false,
+ isUnavailable: false,
},
};
- component.pinnedItems = [component.tree];
fixture.detectChanges();
});
@@ -86,12 +90,72 @@
it('renders view controls', () => {
const viewControls = htmlElement.querySelector('.view-controls');
expect(viewControls).toBeTruthy();
+ const box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy(); //renders at least one view control option
});
- it('renders initial tree elements', async () => {
+ it('disables checkboxes if option unavailable', () => {
+ let box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy();
+ expect((box as HTMLInputElement).disabled).toBeFalse();
+
+ component.userOptions['showDiff'].isUnavailable = true;
+ fixture.detectChanges();
+ box = htmlElement.querySelector('.view-controls input');
+ expect((box as HTMLInputElement).disabled).toBeTrue();
+ });
+
+ it('updates tree on user option checkbox change', () => {
+ const box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy();
+
+ const spy = spyOn(component, 'updateTree');
+ (box as HTMLInputElement).checked = true;
+ (box as HTMLInputElement).dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('renders initial tree elements', () => {
const treeView = htmlElement.querySelector('tree-view');
expect(treeView).toBeTruthy();
- expect(treeView!.innerHTML).toContain('Root node');
- expect(treeView!.innerHTML).toContain('Child node');
+ expect(assertDefined(treeView).innerHTML).toContain('Root node');
+ expect(assertDefined(treeView).innerHTML).toContain('Child node');
+ });
+
+ it('renders pinned nodes', () => {
+ const pinnedNodesDiv = htmlElement.querySelector('.pinned-items');
+ expect(pinnedNodesDiv).toBeFalsy();
+
+ component.pinnedItems = [assertDefined(component.tree)];
+ fixture.detectChanges();
+ const pinnedNodeEl = htmlElement.querySelector('.pinned-items tree-node');
+ expect(pinnedNodeEl).toBeTruthy();
+ });
+
+ it('handles pinned node click', () => {
+ component.pinnedItems = [assertDefined(component.tree)];
+ fixture.detectChanges();
+ const pinnedNodeEl = htmlElement.querySelector('.pinned-items tree-node');
+ expect(pinnedNodeEl).toBeTruthy();
+
+ const propertyTreeChangeSpy = spyOn(component, 'onSelectedTreeChange');
+ const highlightedChangeSpy = spyOn(component, 'onHighlightedItemChange');
+ (pinnedNodeEl as HTMLButtonElement).click();
+ fixture.detectChanges();
+ expect(propertyTreeChangeSpy).toHaveBeenCalled();
+ expect(highlightedChangeSpy).toHaveBeenCalled();
+ });
+
+ it('handles change in filter', () => {
+ const inputEl = htmlElement.querySelector('.title-filter input');
+ expect(inputEl).toBeTruthy();
+
+ const spy = spyOn(component, 'filterTree');
+ (inputEl as HTMLInputElement).value = 'Root';
+ (inputEl as HTMLInputElement).dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ expect(component.filterString).toBe('Root');
});
});
diff --git a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
index 8b63ad5..4559bfc 100644
--- a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
+++ b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
@@ -158,7 +158,7 @@
*ngIf="wmProtoOrNull()"
color="primary"
mat-button
- class="group-header"
+ class="group-header wm-state"
[class]="{selected: isHighlighted(wmProtoOrNull())}"
(click)="onClickShowInPropertiesPanel(wmProtoOrNull(), additionalProperties.wm?.name)">
WMState
@@ -230,7 +230,7 @@
<button
color="primary"
mat-button
- class="group-header"
+ class="group-header ime-container"
[class]="{selected: isHighlighted(additionalProperties.sf.imeContainer)}"
(click)="onClickShowInPropertiesPanel(additionalProperties.sf.imeContainer)">
Ime Container
@@ -252,7 +252,7 @@
<button
color="primary"
mat-button
- class="group-header"
+ class="group-header input-method-surface"
[class]="{selected: isHighlighted(additionalProperties.sf.inputMethodSurface)}"
(click)="onClickShowInPropertiesPanel(additionalProperties.sf.inputMethodSurface)">
Input Method Surface
@@ -321,12 +321,12 @@
export class ImeAdditionalPropertiesComponent {
@Input() additionalProperties!: ImeAdditionalProperties;
@Input() isImeManagerService?: boolean;
- @Input() highlightedItems: string[] = [];
+ @Input() highlightedItem: string = '';
constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
isHighlighted(item: any) {
- return UiTreeUtils.isHighlighted(item, this.highlightedItems);
+ return UiTreeUtils.isHighlighted(item, this.highlightedItem);
}
formatProto(item: any) {
@@ -458,14 +458,14 @@
}
onClickShowInPropertiesPanel(item: any, name?: string) {
- if (item.id) {
- this.updateHighlightedItems(item.id);
+ if (item.stableId) {
+ this.updateHighlightedItem(item.stableId);
} else {
this.updateAdditionalPropertySelected(item, name);
}
}
- private updateHighlightedItems(newId: string) {
+ private updateHighlightedItem(newId: string) {
const event: CustomEvent = new CustomEvent(ViewerEvents.HighlightedChange, {
bubbles: true,
detail: {id: newId},
diff --git a/tools/winscope/src/viewers/components/ime_additional_properties_component_test.ts b/tools/winscope/src/viewers/components/ime_additional_properties_component_test.ts
index 501c22c..c7ec80f 100644
--- a/tools/winscope/src/viewers/components/ime_additional_properties_component_test.ts
+++ b/tools/winscope/src/viewers/components/ime_additional_properties_component_test.ts
@@ -13,30 +13,106 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDividerModule} from '@angular/material/divider';
+import {assertDefined} from 'common/assert_utils';
+import {ImeAdditionalProperties} from 'viewers/common/ime_additional_properties';
+import {ViewerEvents} from 'viewers/common/viewer_events';
+import {CoordinatesTableComponent} from './coordinates_table_component';
import {ImeAdditionalPropertiesComponent} from './ime_additional_properties_component';
describe('ImeAdditionalPropertiesComponent', () => {
- let fixture: ComponentFixture<ImeAdditionalPropertiesComponent>;
- let component: ImeAdditionalPropertiesComponent;
+ let fixture: ComponentFixture<TestHostComponent>;
+ let component: TestHostComponent;
let htmlElement: HTMLElement;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MatDividerModule],
- declarations: [ImeAdditionalPropertiesComponent],
- schemas: [],
+ declarations: [
+ ImeAdditionalPropertiesComponent,
+ TestHostComponent,
+ CoordinatesTableComponent,
+ ],
}).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(ImeAdditionalPropertiesComponent);
+ fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
+ htmlElement.addEventListener(ViewerEvents.HighlightedChange, component.onHighlightedChange);
+ htmlElement.addEventListener(
+ ViewerEvents.AdditionalPropertySelected,
+ component.onAdditionalPropertySelectedChange
+ );
+ fixture.detectChanges();
});
it('can be created', () => {
expect(component).toBeTruthy();
});
+
+ it('emits update additional property tree event on wm state button click', () => {
+ const button: HTMLButtonElement | null = htmlElement.querySelector('.wm-state');
+ assertDefined(button).click();
+ fixture.detectChanges();
+ expect(component.additionalPropertieTree).toBe('wmState');
+ });
+
+ it('propagates new ime container layer on button click', () => {
+ const button: HTMLButtonElement | null = htmlElement.querySelector('.ime-container');
+ assertDefined(button).click();
+ fixture.detectChanges();
+ expect(component.highlightedItem).toBe('imeContainerId');
+ });
+
+ it('propagates new input method surface layer on button click', () => {
+ const button: HTMLButtonElement | null = htmlElement.querySelector('.input-method-surface');
+ assertDefined(button).click();
+ fixture.detectChanges();
+ expect(component.highlightedItem).toBe('inputMethodSurfaceId');
+ });
+
+ @Component({
+ selector: 'host-component',
+ template: `
+ <ime-additional-properties
+ [highlightedItem]="highlightedItem"
+ [isImeManagerService]="false"
+ [additionalProperties]="additionalProperties"></ime-additional-properties>
+ `,
+ })
+ class TestHostComponent {
+ additionalProperties = new ImeAdditionalProperties(
+ {
+ name: 'wmState',
+ stableId: 'wmStateId',
+ focusedApp: 'exampleFocusedApp',
+ focusedWindow: null,
+ focusedActivity: null,
+ isInputMethodWindowVisible: false,
+ protoImeControlTarget: null,
+ protoImeInputTarget: null,
+ protoImeLayeringTarget: null,
+ protoImeInsetsSourceProvider: null,
+ proto: {name: 'wmStateProto'},
+ },
+ {
+ name: 'imeLayers',
+ imeContainer: {name: 'imeContainer', stableId: 'imeContainerId'},
+ inputMethodSurface: {name: 'inputMethodSurface', stableId: 'inputMethodSurfaceId'},
+ focusedWindow: undefined,
+ taskOfImeContainer: undefined,
+ taskOfImeSnapshot: undefined,
+ }
+ );
+ highlightedItem = '';
+ additionalPropertieTree = '';
+
+ onHighlightedChange = (event: Event) => {
+ this.highlightedItem = (event as CustomEvent).detail.id;
+ };
+ onAdditionalPropertySelectedChange = (event: Event) => {
+ this.additionalPropertieTree = (event as CustomEvent).detail.selectedItem.name;
+ };
+ }
});
diff --git a/tools/winscope/src/viewers/components/properties_component.ts b/tools/winscope/src/viewers/components/properties_component.ts
index 9162c04..569843f 100644
--- a/tools/winscope/src/viewers/components/properties_component.ts
+++ b/tools/winscope/src/viewers/components/properties_component.ts
@@ -15,9 +15,11 @@
*/
import {Component, ElementRef, Inject, Input} from '@angular/core';
import {TraceTreeNode} from 'trace/trace_tree_node';
+import {TraceType, ViewNode} from 'trace/trace_type';
import {PropertiesTreeNode, Terminal} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
import {ViewerEvents} from 'viewers/common/viewer_events';
+import {nodeStyles} from 'viewers/components/styles/node.styles';
@Component({
selector: 'properties-view',
@@ -25,10 +27,8 @@
<div class="view-header" [class.view-header-with-property-groups]="displayPropertyGroups">
<div class="title-filter">
<h2 class="properties-title mat-title">Properties</h2>
-
- <mat-form-field>
+ <mat-form-field (keydown.enter)="$event.target.blur()">
<mat-label>Filter...</mat-label>
-
<input matInput [(ngModel)]="filterString" (ngModelChange)="filterTree()" name="filter" />
</mat-form-field>
</div>
@@ -38,16 +38,22 @@
*ngFor="let option of objectKeys(userOptions)"
color="primary"
[(ngModel)]="userOptions[option].enabled"
+ [disabled]="userOptions[option].isUnavailable ?? false"
(ngModelChange)="updateTree()"
[matTooltip]="userOptions[option].tooltip ?? ''"
>{{ userOptions[option].name }}</mat-checkbox
>
</div>
- <property-groups
- *ngIf="itemIsSelected() && displayPropertyGroups"
+ <surface-flinger-property-groups
+ *ngIf="itemIsSelected() && isSurfaceFlinger() && displayPropertyGroups"
class="property-groups"
- [item]="selectedFlickerItem"></property-groups>
+ [item]="selectedItem"></surface-flinger-property-groups>
+
+ <view-capture-property-groups
+ *ngIf="showViewCaptureFormat()"
+ class="property-groups"
+ [item]="selectedItem"></view-capture-property-groups>
</div>
<mat-divider></mat-divider>
@@ -61,9 +67,12 @@
<div class="tree-wrapper">
<tree-view
- *ngIf="objectKeys(propertiesTree).length > 0"
+ *ngIf="objectKeys(propertiesTree).length > 0 && !showViewCaptureFormat()"
[item]="propertiesTree"
[showNode]="showNode"
+ [itemsClickable]="true"
+ [highlightedItem]="highlightedProperty"
+ (highlightedChange)="onHighlightedPropertyChange($event)"
[isLeaf]="isLeaf"
[isAlwaysCollapsed]="true"></tree-view>
</div>
@@ -113,6 +122,7 @@
overflow: auto;
}
`,
+ nodeStyles,
],
})
export class PropertiesComponent {
@@ -121,9 +131,11 @@
@Input() userOptions: UserOptions = {};
@Input() propertiesTree: PropertiesTreeNode = {};
- @Input() selectedFlickerItem: TraceTreeNode | null = null;
+ @Input() highlightedProperty: string = '';
+ @Input() selectedItem: TraceTreeNode | ViewNode | null = null;
@Input() displayPropertyGroups = false;
@Input() isProtoDump = false;
+ @Input() traceType: TraceType | undefined;
constructor(@Inject(ElementRef) private elementRef: ElementRef) {}
@@ -135,6 +147,14 @@
this.elementRef.nativeElement.dispatchEvent(event);
}
+ onHighlightedPropertyChange(newId: string) {
+ const event: CustomEvent = new CustomEvent(ViewerEvents.HighlightedPropertyChange, {
+ bubbles: true,
+ detail: {id: newId},
+ });
+ this.elementRef.nativeElement.dispatchEvent(event);
+ }
+
updateTree() {
const event: CustomEvent = new CustomEvent(ViewerEvents.PropertiesUserOptionsChange, {
bubbles: true,
@@ -160,6 +180,20 @@
}
itemIsSelected() {
- return this.selectedFlickerItem && Object.keys(this.selectedFlickerItem).length > 0;
+ return this.selectedItem && Object.keys(this.selectedItem).length > 0;
+ }
+
+ showViewCaptureFormat(): boolean {
+ return (
+ this.traceType === TraceType.VIEW_CAPTURE &&
+ this.filterString === '' &&
+ // Todo: Highlight Inline in formatted ViewCapture Properties Component.
+ this.userOptions['showDiff']?.enabled === false &&
+ this.selectedItem
+ );
+ }
+
+ isSurfaceFlinger(): boolean {
+ return this.traceType === TraceType.SURFACE_FLINGER;
}
}
diff --git a/tools/winscope/src/viewers/components/properties_component_test.ts b/tools/winscope/src/viewers/components/properties_component_test.ts
index 9098ff3..f118575 100644
--- a/tools/winscope/src/viewers/components/properties_component_test.ts
+++ b/tools/winscope/src/viewers/components/properties_component_test.ts
@@ -16,13 +16,14 @@
import {CommonModule} from '@angular/common';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {PropertiesComponent} from './properties_component';
-import {PropertyGroupsComponent} from './property_groups_component';
+import {SurfaceFlingerPropertyGroupsComponent} from './surface_flinger_property_groups_component';
import {TreeComponent} from './tree_component';
describe('PropertiesComponent', () => {
@@ -30,10 +31,10 @@
let component: PropertiesComponent;
let htmlElement: HTMLElement;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
- declarations: [PropertiesComponent, PropertyGroupsComponent, TreeComponent],
+ declarations: [PropertiesComponent, SurfaceFlingerPropertyGroupsComponent, TreeComponent],
imports: [
CommonModule,
MatInputModule,
@@ -41,45 +42,83 @@
MatCheckboxModule,
MatDividerModule,
BrowserAnimationsModule,
+ FormsModule,
+ ReactiveFormsModule,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
- });
- beforeEach(() => {
fixture = TestBed.createComponent(PropertiesComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
- component.propertiesTree = {};
- component.selectedFlickerItem = null;
+
component.userOptions = {
- showDefaults: {
- name: 'Show defaults',
+ showDiff: {
+ name: 'Show diff',
enabled: false,
+ isUnavailable: false,
},
};
+
+ fixture.detectChanges();
});
it('can be created', () => {
- fixture.detectChanges();
expect(component).toBeTruthy();
});
it('creates title', () => {
- fixture.detectChanges();
const title = htmlElement.querySelector('.properties-title');
expect(title).toBeTruthy();
});
- it('creates view controls', () => {
- fixture.detectChanges();
+ it('renders view controls', () => {
const viewControls = htmlElement.querySelector('.view-controls');
expect(viewControls).toBeTruthy();
+ const box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy(); //renders at least one view control option
});
- it('creates initial tree elements', () => {
+ it('disables checkboxes if option unavailable', () => {
+ let box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy();
+ expect((box as HTMLInputElement).disabled).toBeFalse();
+
+ component.userOptions['showDiff'].isUnavailable = true;
fixture.detectChanges();
- const tree = htmlElement.querySelector('.tree-wrapper');
- expect(tree).toBeTruthy();
+ box = htmlElement.querySelector('.view-controls input');
+ expect((box as HTMLInputElement).disabled).toBeTrue();
+ });
+
+ it('updates tree on user option checkbox change', () => {
+ const box = htmlElement.querySelector('.view-controls input');
+ expect(box).toBeTruthy();
+
+ const spy = spyOn(component, 'updateTree');
+ (box as HTMLInputElement).checked = true;
+ (box as HTMLInputElement).dispatchEvent(new Event('click'));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('renders tree in proto dump upon selected item', () => {
+ component.propertiesTree = {
+ stableId: 'selectedItemProperty',
+ };
+ fixture.detectChanges();
+ const treeEl = htmlElement.querySelector('tree-view');
+ expect(treeEl).toBeTruthy();
+ });
+
+ it('handles change in filter', () => {
+ const inputEl = htmlElement.querySelector('.title-filter input');
+ expect(inputEl).toBeTruthy();
+
+ const spy = spyOn(component, 'filterTree');
+ (inputEl as HTMLInputElement).value = 'Root';
+ (inputEl as HTMLInputElement).dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ expect(component.filterString).toBe('Root');
});
});
diff --git a/tools/winscope/src/viewers/components/property_groups_component.ts b/tools/winscope/src/viewers/components/property_groups_component.ts
deleted file mode 100644
index 5193de1..0000000
--- a/tools/winscope/src/viewers/components/property_groups_component.ts
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * 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.
- */
-import {Component, Input} from '@angular/core';
-import {Layer} from 'trace/flickerlib/common';
-
-@Component({
- selector: 'property-groups',
- template: `
- <div class="group">
- <h3 class="group-header mat-subheading-2">Visibility</h3>
- <div class="left-column">
- <p class="mat-body-1 flags">
- <span class="mat-body-2">Flags:</span>
- &ngsp;
- {{ item.verboseFlags ? item.verboseFlags : item.flags }}
- </p>
- <p *ngFor="let reason of summary()" class="mat-body-1">
- <span class="mat-body-2">{{ reason.key }}:</span>
- &ngsp;
- {{ reason.value }}
- </p>
- </div>
- </div>
- <mat-divider></mat-divider>
- <div class="group">
- <h3 class="group-header mat-subheading-2">Geometry</h3>
- <div class="left-column">
- <p class="column-header mat-small">Calculated</p>
- <p class="property mat-body-2">Transform:</p>
- <transform-matrix
- [transform]="item.transform"
- [formatFloat]="formatFloat"></transform-matrix>
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="Raw value read from proto.bounds. This is the buffer size or
- requested crop cropped by parent bounds."
- >Crop:</span
- >
- &ngsp;
- {{ item.bounds }}
- </p>
-
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="Raw value read from proto.screenBounds. This is the calculated crop
- transformed."
- >Final Bounds:</span
- >
- &ngsp;
- {{ item.screenBounds }}
- </p>
- </div>
- <div class="right-column">
- <p class="column-header mat-small">Requested</p>
- <p class="property mat-body-2">Transform:</p>
- <transform-matrix
- [transform]="item.requestedTransform"
- [formatFloat]="formatFloat"></transform-matrix>
- <p class="mat-body-1">
- <span class="mat-body-2">Crop:</span>
- &ngsp;
- {{ item.crop ? item.crop : '[empty]' }}
- </p>
- </div>
- </div>
- <mat-divider></mat-divider>
- <div class="group">
- <h3 class="group-header mat-subheading-2">Buffer</h3>
- <div class="left-column">
- <p class="mat-body-1">
- <span class="mat-body-2">Size:</span>
- &ngsp;
- {{ item.activeBuffer }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Frame Number:</span>
- &ngsp;
- {{ item.currFrame }}
- </p>
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="Rotates or flips the buffer in place. Used with display transform
- hint to cancel out any buffer transformation when sending to
- HWC."
- >Transform:</span
- >
- &ngsp;
- {{ item.bufferTransform }}
- </p>
- </div>
- <div class="right-column">
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="Scales buffer to the frame by overriding the requested transform
- for this item."
- >Destination Frame:</span
- >
- &ngsp;
- {{ getDestinationFrame() }}
- </p>
- <p *ngIf="hasIgnoreDestinationFrame()" class="mat-body-1">
- Destination Frame ignored because item has eIgnoreDestinationFrame flag set.
- </p>
- </div>
- </div>
- <mat-divider></mat-divider>
- <div class="group">
- <h3 class="group-header mat-subheading-2">Hierarchy</h3>
- <div class="left-column">
- <p class="mat-body-1">
- <span class="mat-body-2">z-order:</span>
- &ngsp;
- {{ item.z }}
- </p>
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="item is z-ordered relative to its relative parents but its bounds
- and other properties are inherited from its parents."
- >relative parent:</span
- >
- &ngsp;
- {{ item.zOrderRelativeOfId == -1 ? 'none' : item.zOrderRelativeOfId }}
- </p>
- </div>
- </div>
- <mat-divider></mat-divider>
- <div class="group">
- <h3 class="group-header mat-subheading-2">Effects</h3>
- <div class="left-column">
- <p class="column-header mat-small">Calculated</p>
- <p class="mat-body-1">
- <span class="mat-body-2">Color:</span>
- &ngsp;
- {{ item.color }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Shadow:</span>
- &ngsp;
- {{ item.shadowRadius }} px
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Corner Radius:</span>
- &ngsp;
- {{ formatFloat(item.cornerRadius) }} px
- </p>
- <p class="mat-body-1">
- <span
- class="mat-body-2"
- matTooltip="Crop used to define the bounds of the corner radii. If the bounds
- are greater than the item bounds then the rounded corner will not
- be visible."
- >Corner Radius Crop:</span
- >
- &ngsp;
- {{ item.cornerRadiusCrop }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Blur:</span>
- &ngsp;
- {{ item.proto?.backgroundBlurRadius ? item.proto?.backgroundBlurRadius : 0 }} px
- </p>
- </div>
- <div class="right-column">
- <p class="column-header mat-small">Requested</p>
- <p class="mat-body-1">
- <span class="mat-body-2">Color:</span>
- &ngsp;
- {{ item.requestedColor }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Shadow:</span>
- &ngsp;
- {{ item.proto?.requestedShadowRadius ? item.proto?.requestedShadowRadius : 0 }} px
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Corner Radius:</span>
- &ngsp;
- {{
- item.proto?.requestedCornerRadius ? formatFloat(item.proto?.requestedCornerRadius) : 0
- }}
- px
- </p>
- </div>
- </div>
- <mat-divider></mat-divider>
- <div class="group">
- <h3 class="group-header mat-subheading-2">Input</h3>
- <ng-container *ngIf="hasInputChannel()">
- <div class="left-column">
- <p class="property mat-body-2">To Display Transform:</p>
- <transform-matrix
- [transform]="item.inputTransform"
- [formatFloat]="formatFloat"></transform-matrix>
- <p class="mat-body-1">
- <span class="mat-body-2">Touchable Region:</span>
- &ngsp;
- {{ item.inputRegion }}
- </p>
- </div>
- <div class="right-column">
- <p class="column-header mat-small">Config</p>
- <p class="mat-body-1">
- <span class="mat-body-2">Focusable:</span>
- &ngsp;
- {{ item.proto?.inputWindowInfo.focusable }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Crop touch region with item:</span>
- &ngsp;
- {{
- item.proto?.inputWindowInfo.cropLayerId <= 0
- ? "none"
- : item.proto?.inputWindowInfo.cropLayerId
- }}
- </p>
- <p class="mat-body-1">
- <span class="mat-body-2">Replace touch region with crop:</span>
- &ngsp;
- {{ item.proto?.inputWindowInfo.replaceTouchableRegionWithCrop }}
- </p>
- </div>
- </ng-container>
- <div *ngIf="!hasInputChannel()" class="left-column">
- <p class="mat-body-1">
- <span class="mat-body-2">Input channel:</span>
- &ngsp; not set
- </p>
- </div>
- </div>
- `,
- styles: [
- `
- .group {
- display: flex;
- flex-direction: row;
- padding: 8px;
- }
-
- .group-header {
- width: 80px;
- color: gray;
- }
-
- .left-column {
- flex: 1;
- padding: 0 5px;
- }
-
- .right-column {
- flex: 1;
- border: 1px solid var(--border-color);
- border-left-width: 5px;
- padding: 0 5px;
- }
-
- .column-header {
- color: gray;
- }
- `,
- ],
-})
-export class PropertyGroupsComponent {
- @Input() item!: Layer;
-
- hasInputChannel() {
- return this.item.proto?.inputWindowInfo;
- }
-
- getDestinationFrame() {
- const frame = this.item.proto?.destinationFrame;
- if (frame) {
- return ` left: ${frame.left}, top: ${frame.top}, right: ${frame.right}, bottom: ${frame.bottom}`;
- } else return '';
- }
-
- hasIgnoreDestinationFrame() {
- return (this.item.flags & 0x400) === 0x400;
- }
-
- formatFloat(num: number) {
- return Math.round(num * 100) / 100;
- }
-
- summary(): TreeSummary {
- const summary = [];
-
- if (this.item?.visibilityReason?.length > 0) {
- let reason = '';
- if (Array.isArray(this.item.visibilityReason)) {
- reason = this.item.visibilityReason.join(', ');
- } else {
- reason = this.item.visibilityReason;
- }
-
- summary.push({key: 'Invisible due to', value: reason});
- }
-
- if (this.item?.occludedBy?.length > 0) {
- summary.push({
- key: 'Occluded by',
- value: this.item.occludedBy.map((it: any) => it.id).join(', '),
- });
- }
-
- if (this.item?.partiallyOccludedBy?.length > 0) {
- summary.push({
- key: 'Partially occluded by',
- value: this.item.partiallyOccludedBy.map((it: any) => it.id).join(', '),
- });
- }
-
- if (this.item?.coveredBy?.length > 0) {
- summary.push({
- key: 'Covered by',
- value: this.item.coveredBy.map((it: any) => it.id).join(', '),
- });
- }
-
- return summary;
- }
-}
-
-type TreeSummary = Array<{key: string; value: string}>;
diff --git a/tools/winscope/src/viewers/components/property_groups_component_test.ts b/tools/winscope/src/viewers/components/property_groups_component_test.ts
deleted file mode 100644
index 1186061..0000000
--- a/tools/winscope/src/viewers/components/property_groups_component_test.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.
- */
-import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {MatDividerModule} from '@angular/material/divider';
-import {MatTooltipModule} from '@angular/material/tooltip';
-import {LayerBuilder} from 'test/unit/layer_builder';
-import {PropertyGroupsComponent} from './property_groups_component';
-import {TransformMatrixComponent} from './transform_matrix_component';
-
-describe('PropertyGroupsComponent', () => {
- let fixture: ComponentFixture<PropertyGroupsComponent>;
- let component: PropertyGroupsComponent;
- let htmlElement: HTMLElement;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [MatDividerModule, MatTooltipModule],
- declarations: [PropertyGroupsComponent, TransformMatrixComponent],
- schemas: [],
- }).compileComponents();
- fixture = TestBed.createComponent(PropertyGroupsComponent);
- component = fixture.componentInstance;
- htmlElement = fixture.nativeElement;
- });
-
- it('can be created', () => {
- expect(component).toBeTruthy();
- });
-
- it('it renders verbose flags if available', async () => {
- const layer = new LayerBuilder().setFlags(3).build();
- component.item = layer;
- fixture.detectChanges();
-
- const flags = htmlElement.querySelector('.flags');
- expect(flags).toBeTruthy();
- expect(flags!.innerHTML).toMatch('Flags:.*HIDDEN|OPAQUE \\(0x3\\)');
- });
-
- it('it renders numeric flags if verbose flags not available', async () => {
- const layer = new LayerBuilder().setFlags(0).build();
- component.item = layer;
- fixture.detectChanges();
-
- const flags = htmlElement.querySelector('.flags');
- expect(flags).toBeTruthy();
- expect(flags!.innerHTML).toMatch('Flags:.*0');
- });
-});
diff --git a/tools/winscope/src/viewers/components/rects/canvas.ts b/tools/winscope/src/viewers/components/rects/canvas.ts
index 078c9ee..e0436f8 100644
--- a/tools/winscope/src/viewers/components/rects/canvas.ts
+++ b/tools/winscope/src/viewers/components/rects/canvas.ts
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {TransformMatrix} from 'common/geometry_utils';
import * as THREE from 'three';
import {CSS2DObject, CSS2DRenderer} from 'three/examples/jsm/renderers/CSS2DRenderer';
-import {Rectangle} from 'viewers/common/rectangle';
import {ViewerEvents} from 'viewers/common/viewer_events';
-import {Circle3D, ColorType, Label3D, Point3D, Rect3D, Scene3D, Transform3D} from './types3d';
+import {Circle3D, ColorType, Label3D, Point3D, Rect3D, Scene3D} from './types3d';
export class Canvas {
- private static readonly TARGET_SCENE_DIAGONAL = 4;
+ static readonly TARGET_SCENE_DIAGONAL = 4;
private static readonly RECT_COLOR_HIGHLIGHTED = new THREE.Color(0xd2e3fc);
+ private static readonly RECT_COLOR_HAS_CONTENT = new THREE.Color(0xad42f5);
private static readonly RECT_EDGE_COLOR = 0x000000;
private static readonly RECT_EDGE_COLOR_ROUNDED = 0x848884;
private static readonly LABEL_CIRCLE_COLOR = 0x000000;
@@ -31,15 +32,14 @@
private static readonly OPACITY_OVERSIZED = 0.25;
private canvasRects: HTMLCanvasElement;
- private canvasLabels: HTMLElement;
+ private canvasLabels?: HTMLElement;
private camera?: THREE.OrthographicCamera;
private scene?: THREE.Scene;
private renderer?: THREE.WebGLRenderer;
private labelRenderer?: CSS2DRenderer;
- private rects: Rectangle[] = [];
private clickableObjects: THREE.Object3D[] = [];
- constructor(canvasRects: HTMLCanvasElement, canvasLabels: HTMLElement) {
+ constructor(canvasRects: HTMLCanvasElement, canvasLabels?: HTMLElement) {
this.canvasRects = canvasRects;
this.canvasLabels = canvasLabels;
}
@@ -98,20 +98,20 @@
alpha: true,
});
- this.labelRenderer = new CSS2DRenderer({element: this.canvasLabels});
-
// set various factors for shading and shifting
- const numberOfRects = this.rects.length;
this.drawRects(scene.rects);
- this.drawLabels(scene.labels);
+ if (this.canvasLabels) {
+ this.drawLabels(scene.labels);
+
+ this.labelRenderer = new CSS2DRenderer({element: this.canvasLabels});
+ this.labelRenderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
+ this.labelRenderer.render(this.scene, this.camera);
+ }
this.renderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.compile(this.scene, this.camera);
this.renderer.render(this.scene, this.camera);
-
- this.labelRenderer.setSize(this.canvasRects!.clientWidth, this.canvasRects!.clientHeight);
- this.labelRenderer.render(this.scene, this.camera);
}
getClickedRectId(x: number, y: number, z: number): undefined | string {
@@ -128,8 +128,8 @@
private drawRects(rects: Rect3D[]) {
this.clickableObjects = [];
rects.forEach((rect) => {
- const rectMesh = this.makeRectMesh(rect);
- const transform = this.toMatrix4(rect.transform);
+ const rectMesh = Canvas.makeRectMesh(rect);
+ const transform = Canvas.toMatrix4(rect.transform);
rectMesh.applyMatrix4(transform);
this.scene?.add(rectMesh);
@@ -186,7 +186,7 @@
div.style.pointerEvents = 'auto';
div.style.cursor = 'pointer';
div.addEventListener('click', (event) =>
- this.propagateUpdateHighlightedItems(event, label.rectId)
+ this.propagateUpdateHighlightedItem(event, label.rectId)
);
const labelCss = new CSS2DObject(div);
@@ -195,7 +195,7 @@
this.scene?.add(labelCss);
}
- private toMatrix4(transform: Transform3D): THREE.Matrix4 {
+ private static toMatrix4(transform: TransformMatrix): THREE.Matrix4 {
return new THREE.Matrix4().set(
transform.dsdx,
transform.dsdy,
@@ -216,10 +216,10 @@
);
}
- private makeRectMesh(rect: Rect3D): THREE.Mesh {
- const rectShape = this.createRectShape(rect);
+ private static makeRectMesh(rect: Rect3D): THREE.Mesh {
+ const rectShape = Canvas.createRectShape(rect);
const rectGeometry = new THREE.ShapeGeometry(rectShape);
- const rectBorders = this.createRectBorders(rect, rectGeometry);
+ const rectBorders = Canvas.createRectBorders(rect, rectGeometry);
let opacity = Canvas.OPACITY_REGULAR;
if (rect.isOversized) {
@@ -230,7 +230,7 @@
const mesh = new THREE.Mesh(
rectGeometry,
new THREE.MeshBasicMaterial({
- color: this.getColor(rect),
+ color: Canvas.getColor(rect),
opacity,
transparent: true,
})
@@ -245,7 +245,7 @@
return mesh;
}
- private createRectShape(rect: Rect3D): THREE.Shape {
+ private static createRectShape(rect: Rect3D): THREE.Shape {
const bottomLeft: Point3D = {x: rect.topLeft.x, y: rect.bottomRight.y, z: rect.topLeft.z};
const topRight: Point3D = {x: rect.bottomRight.x, y: rect.topLeft.y, z: rect.bottomRight.z};
@@ -283,7 +283,7 @@
);
}
- private getColor(rect: Rect3D): THREE.Color {
+ private static getColor(rect: Rect3D): THREE.Color {
switch (rect.colorType) {
case ColorType.VISIBLE: {
// green (darkness depends on z order)
@@ -302,13 +302,19 @@
case ColorType.HIGHLIGHTED: {
return Canvas.RECT_COLOR_HIGHLIGHTED;
}
+ case ColorType.HAS_CONTENT: {
+ return Canvas.RECT_COLOR_HAS_CONTENT;
+ }
default: {
throw new Error(`Unexpected color type: ${rect.colorType}`);
}
}
}
- private createRectBorders(rect: Rect3D, rectGeometry: THREE.ShapeGeometry): THREE.LineSegments {
+ private static createRectBorders(
+ rect: Rect3D,
+ rectGeometry: THREE.ShapeGeometry
+ ): THREE.LineSegments {
// create line edges for rect
const edgeGeo = new THREE.EdgesGeometry(rectGeometry);
let edgeMaterial: THREE.Material;
@@ -336,7 +342,7 @@
return mesh;
}
- private propagateUpdateHighlightedItems(event: MouseEvent, newId: string) {
+ private propagateUpdateHighlightedItem(event: MouseEvent, newId: string) {
event.preventDefault();
const highlightedChangeEvent: CustomEvent = new CustomEvent(ViewerEvents.HighlightedChange, {
bubbles: true,
@@ -346,6 +352,8 @@
}
private clearLabels() {
- this.canvasLabels.innerHTML = '';
+ if (this.canvasLabels) {
+ this.canvasLabels.innerHTML = '';
+ }
}
}
diff --git a/tools/winscope/src/viewers/components/rects/mapper3d.ts b/tools/winscope/src/viewers/components/rects/mapper3d.ts
index 3e131e1..0643669 100644
--- a/tools/winscope/src/viewers/components/rects/mapper3d.ts
+++ b/tools/winscope/src/viewers/components/rects/mapper3d.ts
@@ -14,20 +14,13 @@
* limitations under the License.
*/
-import {Rectangle, Size} from 'viewers/common/rectangle';
-import {
- Box3D,
- ColorType,
- Distance2D,
- Label3D,
- Point3D,
- Rect3D,
- Scene3D,
- Transform3D,
-} from './types3d';
+import {IDENTITY_MATRIX, TransformMatrix} from 'common/geometry_utils';
+import {Size, UiRect} from 'viewers/components/rects/types2d';
+import {Box3D, ColorType, Distance2D, Label3D, Point3D, Rect3D, Scene3D} from './types3d';
class Mapper3D {
private static readonly CAMERA_ROTATION_FACTOR_INIT = 1;
+ private static readonly Z_FIGHTING_EPSILON = 5;
private static readonly Z_SPACING_FACTOR_INIT = 1;
private static readonly Z_SPACING_MAX = 200;
private static readonly LABEL_FIRST_Y_OFFSET = 100;
@@ -37,17 +30,9 @@
private static readonly ZOOM_FACTOR_MIN = 0.1;
private static readonly ZOOM_FACTOR_MAX = 8.5;
private static readonly ZOOM_FACTOR_STEP = 0.2;
- private static readonly IDENTITY_TRANSFORM: Transform3D = {
- dsdx: 1,
- dsdy: 0,
- tx: 0,
- dtdx: 0,
- dtdy: 1,
- ty: 0,
- };
- private rects: Rectangle[] = [];
- private highlightedRectIds: string[] = [];
+ private rects: UiRect[] = [];
+ private highlightedRectId: string = '';
private cameraRotationFactor = Mapper3D.CAMERA_ROTATION_FACTOR_INIT;
private zSpacingFactor = Mapper3D.Z_SPACING_FACTOR_INIT;
private zoomFactor = Mapper3D.ZOOM_FACTOR_INIT;
@@ -56,12 +41,12 @@
private showVirtualMode = false; // by default don't show virtual displays
private currentDisplayId = 0; // default stack id is usually 0
- setRects(rects: Rectangle[]) {
+ setRects(rects: UiRect[]) {
this.rects = rects;
}
- setHighlightedRectIds(ids: string[]) {
- this.highlightedRectIds = ids;
+ setHighlightedRectId(id: string) {
+ this.highlightedRectId = id;
}
getCameraRotationFactor(): number {
@@ -80,13 +65,13 @@
this.zSpacingFactor = Math.min(Math.max(factor, 0), 1);
}
- increaseZoomFactor() {
- this.zoomFactor += Mapper3D.ZOOM_FACTOR_STEP;
+ increaseZoomFactor(times: number = 1) {
+ this.zoomFactor += Mapper3D.ZOOM_FACTOR_STEP * times;
this.zoomFactor = Math.min(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MAX);
}
- decreaseZoomFactor() {
- this.zoomFactor -= Mapper3D.ZOOM_FACTOR_STEP;
+ decreaseZoomFactor(times: number = 1) {
+ this.zoomFactor -= Mapper3D.ZOOM_FACTOR_STEP * times;
this.zoomFactor = Math.max(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MIN);
}
@@ -147,7 +132,7 @@
return scene;
}
- private selectRectsToDraw(rects: Rectangle[]): Rectangle[] {
+ private selectRectsToDraw(rects: UiRect[]): UiRect[] {
rects = rects.filter((rect) => rect.displayId === this.currentDisplayId);
if (this.showOnlyVisibleMode) {
@@ -161,7 +146,7 @@
return rects;
}
- private computeRects(rects2d: Rectangle[]): Rect3D[] {
+ private computeRects(rects2d: UiRect[]): Rect3D[] {
let visibleRectsSoFar = 0;
let visibleRectsTotal = 0;
let nonVisibleRectsSoFar = 0;
@@ -175,13 +160,25 @@
}
});
- let z = 0;
-
const maxDisplaySize = this.getMaxDisplaySize(rects2d);
+ const depthToCountOfRects = new Map<number, number>();
+ const computeAntiZFightingOffset = (rectDepth: number) => {
+ // Rendering overlapping rects with equal Z value causes Z-fighting (b/307951779).
+ // Here we compute a Z-offset to be applied to the rect to guarantee that
+ // eventually all rects will have unique Z-values.
+ const countOfRectsAtSameDepth = depthToCountOfRects.get(rectDepth) ?? 0;
+ const antiZFightingOffset = countOfRectsAtSameDepth * Mapper3D.Z_FIGHTING_EPSILON;
+ depthToCountOfRects.set(rectDepth, countOfRectsAtSameDepth + 1);
+ return antiZFightingOffset;
+ };
+
+ let z = 0;
const rects3d = rects2d.map((rect2d): Rect3D => {
if (rect2d.depth !== undefined) {
- z = Mapper3D.Z_SPACING_MAX * this.zSpacingFactor * rect2d.depth;
+ z =
+ this.zSpacingFactor *
+ (Mapper3D.Z_SPACING_MAX * rect2d.depth + computeAntiZFightingOffset(rect2d.depth));
} else {
z -= Mapper3D.Z_SPACING_MAX * this.zSpacingFactor;
}
@@ -193,13 +190,13 @@
const rect = {
id: rect2d.id,
topLeft: {
- x: rect2d.topLeft.x,
- y: rect2d.topLeft.y,
+ x: rect2d.x,
+ y: rect2d.y,
z,
},
bottomRight: {
- x: rect2d.bottomRight.x,
- y: rect2d.bottomRight.y,
+ x: rect2d.x + rect2d.w,
+ y: rect2d.y + rect2d.h,
z,
},
isOversized: false,
@@ -207,7 +204,7 @@
darkFactor,
colorType: this.getColorType(rect2d),
isClickable: rect2d.isClickable,
- transform: this.getTransform(rect2d),
+ transform: rect2d.transform ?? IDENTITY_MATRIX,
};
return this.cropOversizedRect(rect, maxDisplaySize);
});
@@ -215,10 +212,12 @@
return rects3d;
}
- private getColorType(rect2d: Rectangle): ColorType {
+ private getColorType(rect2d: UiRect): ColorType {
let colorType: ColorType;
- if (this.highlightedRectIds.includes(rect2d.id)) {
+ if (this.highlightedRectId === rect2d.id) {
colorType = ColorType.HIGHLIGHTED;
+ } else if (rect2d.hasContent === true) {
+ colorType = ColorType.HAS_CONTENT;
} else if (rect2d.isVisible) {
colorType = ColorType.VISIBLE;
} else {
@@ -227,36 +226,15 @@
return colorType;
}
- private getTransform(rect2d: Rectangle): Transform3D {
- let transform: Transform3D;
- if (rect2d.transform?.matrix) {
- transform = {
- dsdx: rect2d.transform.matrix.dsdx,
- dsdy: rect2d.transform.matrix.dsdy,
- tx: rect2d.transform.matrix.tx,
- dtdx: rect2d.transform.matrix.dtdx,
- dtdy: rect2d.transform.matrix.dtdy,
- ty: rect2d.transform.matrix.ty,
- };
- } else {
- transform = Mapper3D.IDENTITY_TRANSFORM;
- }
- return transform;
- }
-
- private getMaxDisplaySize(rects2d: Rectangle[]): Size {
+ private getMaxDisplaySize(rects2d: UiRect[]): Size {
const displays = rects2d.filter((rect2d) => rect2d.isDisplay);
let maxWidth = 0;
let maxHeight = 0;
if (displays.length > 0) {
- maxWidth = Math.max(
- ...displays.map((rect2d): number => Math.abs(rect2d.topLeft.x - rect2d.bottomRight.x))
- );
+ maxWidth = Math.max(...displays.map((rect2d): number => Math.abs(rect2d.w)));
- maxHeight = Math.max(
- ...displays.map((rect2d): number => Math.abs(rect2d.topLeft.y - rect2d.bottomRight.y))
- );
+ maxHeight = Math.max(...displays.map((rect2d): number => Math.abs(rect2d.h)));
}
return {
width: maxWidth,
@@ -288,7 +266,7 @@
return rect3d;
}
- private computeLabels(rects2d: Rectangle[], rects3d: Rect3D[]): Label3D[] {
+ private computeLabels(rects2d: UiRect[], rects3d: Rect3D[]): Label3D[] {
const labels3d: Label3D[] = [];
let labelY =
@@ -307,12 +285,12 @@
const bottomLeft: Point3D = {
x: rect3d.topLeft.x,
- y: rect3d.bottomRight.y,
+ y: rect3d.topLeft.y,
z: rect3d.topLeft.z,
};
const topRight: Point3D = {
x: rect3d.bottomRight.x,
- y: rect3d.topLeft.y,
+ y: rect3d.bottomRight.y,
z: rect3d.bottomRight.z,
};
const lineStarts = [
@@ -336,7 +314,7 @@
z: lineStart.z,
};
- const isHighlighted = this.highlightedRectIds.includes(rect2d.id);
+ const isHighlighted = this.highlightedRectId === rect2d.id;
const label3d: Label3D = {
circle: {
@@ -361,7 +339,7 @@
return labels3d;
}
- private matMultiply(mat: Transform3D, point: Point3D): Point3D {
+ private matMultiply(mat: TransformMatrix, point: Point3D): Point3D {
return {
x: mat.dsdx * point.x + mat.dsdy * point.y + mat.tx,
y: mat.dtdx * point.x + mat.dtdy * point.y + mat.ty,
diff --git a/tools/winscope/src/viewers/components/rects/rects_component.ts b/tools/winscope/src/viewers/components/rects/rects_component.ts
index 54ce85f..4781592 100644
--- a/tools/winscope/src/viewers/components/rects/rects_component.ts
+++ b/tools/winscope/src/viewers/components/rects/rects_component.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {Component, ElementRef, HostListener, Inject, Input, OnDestroy, OnInit} from '@angular/core';
-import {Rectangle} from 'viewers/common/rectangle';
-import {ViewerEvents} from 'viewers/common/viewer_events';
+import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
+import {UiRect} from 'viewers/components/rects/types2d';
import {Canvas} from './canvas';
import {Mapper3D} from './mapper3d';
import {Distance2D} from './types3d';
@@ -86,9 +86,16 @@
<mat-divider></mat-divider>
<div class="rects-content">
<div class="canvas-container">
- <canvas class="canvas-rects" (click)="onRectClick($event)" oncontextmenu="return false">
- </canvas>
- <div class="canvas-labels"></div>
+ <canvas
+ class="large-rects-canvas"
+ (click)="onRectClick($event)"
+ (dblclick)="onRectDblClick($event)"
+ oncontextmenu="return false"></canvas>
+ <div class="large-rects-labels"></div>
+ <canvas
+ class="mini-rects-canvas"
+ (dblclick)="onMiniRectDblClick($event)"
+ oncontextmenu="return false"></canvas>
</div>
<div *ngIf="internalDisplayIds.length > 1" class="display-button-container">
<button
@@ -140,7 +147,7 @@
width: 100%;
position: relative;
}
- .canvas-rects {
+ .large-rects-canvas {
position: absolute;
top: 0;
left: 0;
@@ -148,7 +155,7 @@
height: 100%;
cursor: pointer;
}
- .canvas-labels {
+ .large-rects-labels {
position: absolute;
top: 0;
left: 0;
@@ -162,47 +169,64 @@
flex-wrap: wrap;
column-gap: 10px;
}
+ .mini-rects-canvas {
+ cursor: pointer;
+ width: 30%;
+ height: 30%;
+ top: 16px;
+ display: block;
+ position: absolute;
+ z-index: 1000;
+ }
`,
],
})
export class RectsComponent implements OnInit, OnDestroy {
@Input() title = 'title';
@Input() enableShowVirtualButton: boolean = true;
- @Input() set rects(rects: Rectangle[]) {
+ @Input() zoomFactor: number = 1;
+ @Input() set rects(rects: UiRect[]) {
this.internalRects = rects;
- this.drawScene();
+ this.drawLargeRectsAndLabels();
+ }
+ @Input() set miniRects(rects: UiRect[] | undefined) {
+ this.internalMiniRects = rects;
+ this.drawMiniRects();
}
@Input() set displayIds(ids: number[]) {
this.internalDisplayIds = ids;
if (!this.internalDisplayIds.includes(this.mapper3d.getCurrentDisplayId())) {
this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0]);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
}
- @Input() set highlightedItems(stableIds: string[]) {
- this.internalHighlightedItems = stableIds;
- this.mapper3d.setHighlightedRectIds(this.internalHighlightedItems);
- this.drawScene();
+ @Input() set highlightedItem(stableId: string) {
+ this.internalHighlightedItem = stableId;
+ this.mapper3d.setHighlightedRectId(this.internalHighlightedItem);
+ this.drawLargeRectsAndLabels();
}
- private internalRects: Rectangle[] = [];
+ private internalRects: UiRect[] = [];
+ private internalMiniRects?: UiRect[];
private internalDisplayIds: number[] = [];
- private internalHighlightedItems: string[] = [];
+ private internalHighlightedItem: string = '';
private mapper3d: Mapper3D;
- private canvas?: Canvas;
+ private largeRectsCanvas?: Canvas;
+ private miniRectsCanvas?: Canvas;
private resizeObserver: ResizeObserver;
- private canvasRects?: HTMLCanvasElement;
- private canvasLabels?: HTMLElement;
+ private largeRectsCanvasElement?: HTMLCanvasElement;
+ private miniRectsCanvasElement?: HTMLCanvasElement;
+ private largeRectsLabelsElement?: HTMLElement;
private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
constructor(@Inject(ElementRef) private elementRef: ElementRef) {
this.mapper3d = new Mapper3D();
this.resizeObserver = new ResizeObserver((entries) => {
- this.drawScene();
+ this.drawLargeRectsAndLabels();
});
}
@@ -210,14 +234,26 @@
const canvasContainer = this.elementRef.nativeElement.querySelector('.canvas-container');
this.resizeObserver.observe(canvasContainer);
- this.canvasRects = canvasContainer.querySelector('.canvas-rects')! as HTMLCanvasElement;
- this.canvasLabels = canvasContainer.querySelector('.canvas-labels');
- this.canvas = new Canvas(this.canvasRects, this.canvasLabels!);
-
- this.canvasRects.addEventListener('mousedown', (event) => this.onCanvasMouseDown(event));
+ this.largeRectsCanvasElement = canvasContainer.querySelector(
+ '.large-rects-canvas'
+ )! as HTMLCanvasElement;
+ this.largeRectsLabelsElement = canvasContainer.querySelector('.large-rects-labels');
+ this.largeRectsCanvas = new Canvas(this.largeRectsCanvasElement, this.largeRectsLabelsElement!);
+ this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
+ this.onCanvasMouseDown(event)
+ );
this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0] ?? 0);
- this.drawScene();
+ this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
+ this.drawLargeRectsAndLabels();
+
+ this.miniRectsCanvasElement = canvasContainer.querySelector(
+ '.mini-rects-canvas'
+ )! as HTMLCanvasElement;
+ this.miniRectsCanvas = new Canvas(this.miniRectsCanvasElement);
+ if (this.miniRects) {
+ this.drawMiniRects();
+ }
}
ngOnDestroy() {
@@ -226,17 +262,17 @@
onSeparationSliderChange(factor: number) {
this.mapper3d.setZSpacingFactor(factor);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
onRotationSliderChange(factor: number) {
this.mapper3d.setCameraRotationFactor(factor);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
resetCamera() {
this.mapper3d.resetCamera();
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
@HostListener('wheel', ['$event'])
@@ -256,7 +292,7 @@
onMouseMove(event: MouseEvent) {
const distance = new Distance2D(event.movementX, event.movementY);
this.mapper3d.addPanScreenDistance(distance);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
onMouseUp(event: MouseEvent) {
@@ -274,22 +310,53 @@
onShowOnlyVisibleModeChange(enabled: boolean) {
this.mapper3d.setShowOnlyVisibleMode(enabled);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
onShowVirtualModeChange(enabled: boolean) {
this.mapper3d.setShowVirtualMode(enabled);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
onDisplayIdChange(id: number) {
this.mapper3d.setCurrentDisplayId(id);
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
onRectClick(event: MouseEvent) {
event.preventDefault();
+ const id = this.findClickedRectId(event);
+ if (id !== undefined) {
+ this.notifyHighlightedItem(id);
+ }
+ }
+
+ onRectDblClick(event: MouseEvent) {
+ event.preventDefault();
+
+ const clickedRectId = this.findClickedRectId(event);
+ if (clickedRectId === undefined) {
+ return;
+ }
+
+ this.elementRef.nativeElement.dispatchEvent(
+ new CustomEvent(ViewerEvents.RectsDblClick, {
+ bubbles: true,
+ detail: new RectDblClickDetail(clickedRectId),
+ })
+ );
+ }
+
+ onMiniRectDblClick(event: MouseEvent) {
+ event.preventDefault();
+
+ this.elementRef.nativeElement.dispatchEvent(
+ new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true})
+ );
+ }
+
+ private findClickedRectId(event: MouseEvent): string | undefined {
const canvas = event.target as Element;
const canvasOffset = canvas.getBoundingClientRect();
@@ -297,29 +364,45 @@
const y = -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1;
const z = 0;
- const id = this.canvas?.getClickedRectId(x, y, z);
- if (id !== undefined) {
- this.notifyHighlightedItem(id);
- }
+ return this.largeRectsCanvas?.getClickedRectId(x, y, z);
}
private doZoomIn() {
this.mapper3d.increaseZoomFactor();
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
private doZoomOut() {
this.mapper3d.decreaseZoomFactor();
- this.drawScene();
+ this.drawLargeRectsAndLabels();
}
- private drawScene() {
- // TODO: Re-create scene only when input rects change. With the other input events
- // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
- // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
- // work directly with three.js's meshes.
+ private drawLargeRectsAndLabels() {
+ // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
+ // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
+ // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
+ // work directly with three.js's meshes.
this.mapper3d.setRects(this.internalRects);
- this.canvas?.draw(this.mapper3d.computeScene());
+ this.largeRectsCanvas?.draw(this.mapper3d.computeScene());
+ }
+
+ private drawMiniRects() {
+ // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
+ // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
+ // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
+ // work directly with three.js's meshes.
+ if (this.internalMiniRects) {
+ this.mapper3d.setRects(this.internalMiniRects);
+ this.mapper3d.decreaseZoomFactor(this.zoomFactor - 1);
+ this.miniRectsCanvas?.draw(this.mapper3d.computeScene());
+ this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
+
+ // Mapper internally sets these values to 100%. They need to be reset afterwards
+ if (this.miniRectsCanvasElement) {
+ this.miniRectsCanvasElement.style.width = '25%';
+ this.miniRectsCanvasElement.style.height = '25%';
+ }
+ }
}
private notifyHighlightedItem(id: string) {
diff --git a/tools/winscope/src/viewers/components/rects/rects_component_test.ts b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
index 0196e99..09e87ef 100644
--- a/tools/winscope/src/viewers/components/rects/rects_component_test.ts
+++ b/tools/winscope/src/viewers/components/rects/rects_component_test.ts
@@ -20,8 +20,8 @@
import {MatDividerModule} from '@angular/material/divider';
import {MatRadioModule} from '@angular/material/radio';
import {MatSliderModule} from '@angular/material/slider';
-import {Rectangle} from 'viewers/common/rectangle';
import {RectsComponent} from 'viewers/components/rects/rects_component';
+import {UiRect} from 'viewers/components/rects/types2d';
import {Canvas} from './canvas';
describe('RectsComponent', () => {
@@ -57,30 +57,29 @@
});
it('renders canvas', () => {
- const rectsCanvas = htmlElement.querySelector('.canvas-rects');
+ const rectsCanvas = htmlElement.querySelector('.large-rects-canvas');
expect(rectsCanvas).toBeTruthy();
});
it('draws scene when input data changes', async () => {
spyOn(Canvas.prototype, 'draw').and.callThrough();
- const inputRect: Rectangle = {
- topLeft: {x: 0, y: 0},
- bottomRight: {x: 1, y: -1},
+ const inputRect: UiRect = {
+ x: 0,
+ y: 0,
+ w: 1,
+ h: 1,
label: 'rectangle1',
transform: {
- matrix: {
- dsdx: 1,
- dsdy: 0,
- dtdx: 0,
- dtdy: 1,
- tx: 0,
- ty: 0,
- },
+ dsdx: 1,
+ dsdy: 0,
+ dtdx: 0,
+ dtdy: 1,
+ tx: 0,
+ ty: 0,
},
isVisible: true,
isDisplay: false,
- ref: null,
id: 'test-id-1234',
displayId: 0,
isVirtual: false,
diff --git a/tools/winscope/src/viewers/common/rectangle.ts b/tools/winscope/src/viewers/components/rects/types2d.ts
similarity index 65%
rename from tools/winscope/src/viewers/common/rectangle.ts
rename to tools/winscope/src/viewers/components/rects/types2d.ts
index 5f6b858..fe34955 100644
--- a/tools/winscope/src/viewers/common/rectangle.ts
+++ b/tools/winscope/src/viewers/components/rects/types2d.ts
@@ -14,47 +14,23 @@
* limitations under the License.
*/
-export interface Rectangle {
- topLeft: Point;
- bottomRight: Point;
+import {Rect, TransformMatrix} from 'common/geometry_utils';
+
+export interface UiRect extends Rect {
label: string;
- transform: RectTransform | null;
+ transform?: TransformMatrix;
isVisible: boolean;
isDisplay: boolean;
- ref: any;
id: string;
displayId: number;
isVirtual: boolean;
isClickable: boolean;
cornerRadius: number;
depth?: number;
-}
-
-export interface Point {
- x: number;
- y: number;
+ hasContent?: boolean;
}
export interface Size {
width: number;
height: number;
}
-
-export interface RectTransform {
- matrix?: RectMatrix;
- dsdx?: number;
- dsdy?: number;
- dtdx?: number;
- dtdy?: number;
- tx?: number;
- ty?: number;
-}
-
-export interface RectMatrix {
- dsdx: number;
- dsdy: number;
- dtdx: number;
- dtdy: number;
- tx: number;
- ty: number;
-}
diff --git a/tools/winscope/src/viewers/components/rects/types3d.ts b/tools/winscope/src/viewers/components/rects/types3d.ts
index cb30635..fa06e00 100644
--- a/tools/winscope/src/viewers/components/rects/types3d.ts
+++ b/tools/winscope/src/viewers/components/rects/types3d.ts
@@ -14,10 +14,13 @@
* limitations under the License.
*/
+import {TransformMatrix} from 'common/geometry_utils';
+
export enum ColorType {
VISIBLE,
NOT_VISIBLE,
HIGHLIGHTED,
+ HAS_CONTENT,
}
export class Distance2D {
@@ -40,19 +43,10 @@
darkFactor: number;
colorType: ColorType;
isClickable: boolean;
- transform: Transform3D;
+ transform: TransformMatrix;
isOversized: boolean;
}
-export interface Transform3D {
- dsdx: number;
- dsdy: number;
- tx: number;
- dtdx: number;
- dtdy: number;
- ty: number;
-}
-
export interface Point3D {
x: number;
y: number;
diff --git a/tools/winscope/src/viewers/components/styles/node.styles.ts b/tools/winscope/src/viewers/components/styles/node.styles.ts
index f8d8f9b..0c2b6a5 100644
--- a/tools/winscope/src/viewers/components/styles/node.styles.ts
+++ b/tools/winscope/src/viewers/components/styles/node.styles.ts
@@ -42,13 +42,14 @@
color: white;
}
- .selected {background-color: #365179;color: white;}
+ .selected {background-color: #87ACEC;}
`;
// FIXME: child-hover selector is not working.
export const treeNodeDataViewStyles = `
.node + .children:not(.flattened) {margin-left: 12px;padding-left: 11px;border-left: 1px solid var(--border-color);}
.node.selected + .children {border-left: 1px solid rgb(150, 150, 150);}
+ .node.child-selected + .children {border-left: 1px solid rgb(100, 100, 100);}
.node:hover + .children {border-left: 1px solid rgba(150, 150, 150, 0.75);}
.node.child-hover + .children {border-left: 1px solid #b4b4b4;}
`;
diff --git a/tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts
new file mode 100644
index 0000000..0642e1b
--- /dev/null
+++ b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component.ts
@@ -0,0 +1,428 @@
+/*
+ * 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.
+ */
+import {Component, Input} from '@angular/core';
+import {Color, Layer, toColor, toCropRect, Transform} from 'flickerlib/common';
+
+@Component({
+ selector: 'surface-flinger-property-groups',
+ template: `
+ <div class="group">
+ <h3 class="group-header mat-subheading-2">Visibility</h3>
+ <div class="left-column">
+ <p class="mat-body-1 flags">
+ <span class="mat-body-2">Flags:</span>
+ &ngsp;
+ {{ properties.flags }}
+ </p>
+ <p *ngFor="let reason of summary()" class="mat-body-1">
+ <span class="mat-body-2">{{ reason.key }}:</span>
+ &ngsp;
+ {{ reason.value }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group geometry">
+ <h3 class="group-header mat-subheading-2">Geometry</h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Calculated</p>
+ <p class="property mat-body-2">Transform:</p>
+ <transform-matrix
+ *ngIf="properties.calcTransform"
+ [transform]="properties.calcTransform"
+ [formatFloat]="formatFloat"></transform-matrix>
+ <p class="mat-body-1 crop">
+ <span
+ class="mat-body-2"
+ matTooltip="Raw value read from proto.bounds. This is the buffer size or
+ requested crop cropped by parent bounds."
+ >Crop:</span
+ >
+ &ngsp;
+ {{ properties.calcCrop }}
+ </p>
+
+ <p class="mat-body-1 final-bounds">
+ <span
+ class="mat-body-2"
+ matTooltip="Raw value read from proto.screenBounds. This is the calculated crop
+ transformed."
+ >Final Bounds:</span
+ >
+ &ngsp;
+ {{ properties.finalBounds }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Requested</p>
+ <p class="property mat-body-2">Transform:</p>
+ <transform-matrix
+ *ngIf="properties.reqTransform"
+ [transform]="properties.reqTransform"
+ [formatFloat]="formatFloat"></transform-matrix>
+ <p class="mat-body-1 crop">
+ <span class="mat-body-2">Crop:</span>
+ &ngsp;
+ {{ properties.reqCrop }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group buffer">
+ <h3 class="group-header mat-subheading-2">Buffer</h3>
+ <div class="left-column">
+ <p class="mat-body-1 size">
+ <span class="mat-body-2">Size:</span>
+ &ngsp;
+ {{ properties.bufferSize }}
+ </p>
+ <p class="mat-body-1 frame-number">
+ <span class="mat-body-2">Frame Number:</span>
+ &ngsp;
+ {{ properties.frameNumber }}
+ </p>
+ <p class="mat-body-1 transform">
+ <span
+ class="mat-body-2"
+ matTooltip="Rotates or flips the buffer in place. Used with display transform
+ hint to cancel out any buffer transformation when sending to
+ HWC."
+ >Transform:</span
+ >
+ &ngsp;
+ {{ properties.bufferTransform }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="mat-body-1 dest-frame">
+ <span
+ class="mat-body-2"
+ matTooltip="Scales buffer to the frame by overriding the requested transform
+ for this item."
+ >Destination Frame:</span
+ >
+ &ngsp;
+ {{ properties.destinationFrame }}
+ </p>
+ <p *ngIf="hasIgnoreDestinationFrame()" class="mat-body-1">
+ Destination Frame ignored because item has eIgnoreDestinationFrame flag set.
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group hierarchy-info">
+ <h3 class="group-header mat-subheading-2">Hierarchy</h3>
+ <div class="left-column">
+ <p class="mat-body-1 z-order">
+ <span class="mat-body-2">z-order:</span>
+ &ngsp;
+ {{ properties.z }}
+ </p>
+ <p class="mat-body-1 rel-parent">
+ <span
+ class="mat-body-2"
+ matTooltip="item is z-ordered relative to its relative parents but its bounds
+ and other properties are inherited from its parents."
+ >relative parent:</span
+ >
+ &ngsp;
+ {{ properties.relativeParent }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group effects">
+ <h3 class="group-header mat-subheading-2">Effects</h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Calculated</p>
+ <p class="mat-body-1 color">
+ <span class="mat-body-2">Color:</span>
+ &ngsp;
+ {{ properties.calcColor }}
+ </p>
+ <p class="mat-body-1 shadow">
+ <span class="mat-body-2">Shadow:</span>
+ &ngsp;
+ {{ properties.calcShadowRadius }}
+ </p>
+ <p class="mat-body-1 corner-radius">
+ <span class="mat-body-2">Corner Radius:</span>
+ &ngsp;
+ {{ properties.calcCornerRadius }}
+ </p>
+ <p class="mat-body-1">
+ <span
+ class="mat-body-2"
+ matTooltip="Crop used to define the bounds of the corner radii. If the bounds
+ are greater than the item bounds then the rounded corner will not
+ be visible."
+ >Corner Radius Crop:</span
+ >
+ &ngsp;
+ {{ properties.calcCornerRadiusCrop }}
+ </p>
+ <p class="mat-body-1 blur">
+ <span class="mat-body-2">Blur:</span>
+ &ngsp;
+ {{ properties.backgroundBlurRadius }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Requested</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Color:</span>
+ &ngsp;
+ {{ properties.reqColor }}
+ </p>
+ <p class="mat-body-1 shadow">
+ <span class="mat-body-2">Shadow:</span>
+ &ngsp;
+ {{ properties.reqShadowRadius }}
+ </p>
+ <p class="mat-body-1 corner-radius">
+ <span class="mat-body-2">Corner Radius:</span>
+ &ngsp;
+ {{ properties.reqCornerRadius }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group inputs">
+ <h3 class="group-header mat-subheading-2">Input</h3>
+ <ng-container *ngIf="hasInputChannel()">
+ <div class="left-column">
+ <p class="property mat-body-2">To Display Transform:</p>
+ <transform-matrix
+ *ngIf="properties.inputTransform"
+ [transform]="properties.inputTransform"
+ [formatFloat]="formatFloat"></transform-matrix>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Touchable Region:</span>
+ &ngsp;
+ {{ properties.inputRegion }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Config</p>
+ <p class="mat-body-1 focusable">
+ <span class="mat-body-2">Focusable:</span>
+ &ngsp;
+ {{ properties.focusable }}
+ </p>
+ <p class="mat-body-1 crop-touch-region">
+ <span class="mat-body-2">Crop touch region with item:</span>
+ &ngsp;
+ {{ properties.cropTouchRegionWithItem }}
+ </p>
+ <p class="mat-body-1 replace-touch-region">
+ <span class="mat-body-2">Replace touch region with crop:</span>
+ &ngsp;
+ {{ properties.replaceTouchRegionWithCrop }}
+ </p>
+ </div>
+ </ng-container>
+ <div *ngIf="!hasInputChannel()" class="left-column">
+ <p class="mat-body-1">
+ <span class="mat-body-2">Input channel:</span>
+ &ngsp; not set
+ </p>
+ </div>
+ </div>
+ `,
+ styles: [
+ `
+ .group {
+ display: flex;
+ flex-direction: row;
+ padding: 8px;
+ }
+
+ .group-header {
+ width: 80px;
+ color: gray;
+ }
+
+ .left-column {
+ flex: 1;
+ padding: 0 5px;
+ }
+
+ .right-column {
+ flex: 1;
+ border: 1px solid var(--border-color);
+ border-left-width: 5px;
+ padding: 0 5px;
+ }
+
+ .column-header {
+ color: gray;
+ }
+ `,
+ ],
+})
+export class SurfaceFlingerPropertyGroupsComponent {
+ @Input() item!: Layer;
+ properties: any; // TODO(b/278163557): change to proper type when layer's TreeNode data type is defined
+
+ // TODO(b/278163557): move all properties computation into parser and pass properties as @Input, as soon as layer's TreeNode data type is defined
+ ngOnChanges() {
+ this.properties = {
+ flags: this.item.verboseFlags ? this.item.verboseFlags : this.item.flags,
+ calcTransform: this.item.transform,
+ calcCrop: this.item.bounds,
+ finalBounds: this.item.screenBounds,
+ reqTransform: this.getReqTransform(),
+ reqCrop: this.item.crop ? this.item.crop : '[empty]',
+ bufferSize: this.item.activeBuffer,
+ frameNumber: this.item.currFrame,
+ bufferTransform: this.item.bufferTransform,
+ destinationFrame: this.getDestinationFrame(),
+ z: this.item.z,
+ relativeParent: this.getRelativeParent(),
+ calcColor: this.getCalcColor(),
+ calcShadowRadius: this.getPixelPropertyValue(this.item.shadowRadius),
+ calcCornerRadius: this.getPixelPropertyValue(this.item.cornerRadius),
+ calcCornerRadiusCrop: this.getCalcCornerRadiusCrop(),
+ backgroundBlurRadius: this.getPixelPropertyValue(this.item.backgroundBlurRadius),
+ reqColor: this.getReqColor(),
+ reqShadowRadius: this.getPixelPropertyValue(this.item.proto?.requestedShadowRadius),
+ reqCornerRadius: this.getPixelPropertyValue(this.item.proto?.requestedCornerRadius),
+ inputTransform: this.getInputTransform(),
+ inputRegion: this.item.inputRegion,
+ focusable: this.item.proto?.inputWindowInfo?.focusable,
+ cropTouchRegionWithItem: this.getCropTouchRegionWithItem(),
+ replaceTouchRegionWithCrop: this.item.proto?.inputWindowInfo?.replaceTouchableRegionWithCrop,
+ };
+ }
+
+ hasInputChannel() {
+ return this.item.proto?.inputWindowInfo;
+ }
+
+ hasIgnoreDestinationFrame() {
+ return (this.item.flags & 0x400) === 0x400;
+ }
+
+ formatFloat(num: number) {
+ return Math.round(num * 100) / 100;
+ }
+
+ summary(): TreeSummary {
+ const summary = [];
+
+ if (this.item?.visibilityReason?.length > 0) {
+ let reason = '';
+ if (Array.isArray(this.item.visibilityReason)) {
+ reason = this.item.visibilityReason.join(', ');
+ } else {
+ reason = this.item.visibilityReason;
+ }
+
+ summary.push({key: 'Invisible due to', value: reason});
+ }
+
+ if (this.item?.occludedBy?.length > 0) {
+ summary.push({
+ key: 'Occluded by',
+ value: this.item.occludedBy.map((it: any) => it.id).join(', '),
+ });
+ }
+
+ if (this.item?.partiallyOccludedBy?.length > 0) {
+ summary.push({
+ key: 'Partially occluded by',
+ value: this.item.partiallyOccludedBy.map((it: any) => it.id).join(', '),
+ });
+ }
+
+ if (this.item?.coveredBy?.length > 0) {
+ summary.push({
+ key: 'Covered by',
+ value: this.item.coveredBy.map((it: any) => it.id).join(', '),
+ });
+ }
+
+ return summary;
+ }
+
+ private getReqTransform() {
+ const proto = this.item.proto;
+ return Transform.fromProto(proto?.requestedTransform, proto?.requestedPosition);
+ }
+
+ private getDestinationFrame() {
+ const frame = this.item.proto?.destinationFrame;
+ if (frame) {
+ return ` left: ${frame.left}, top: ${frame.top}, right: ${frame.right}, bottom: ${frame.bottom}`;
+ }
+ return '[empty]';
+ }
+
+ private getCalcCornerRadiusCrop() {
+ if (this.item.proto?.cornerRadiusCrop) {
+ return toCropRect(this.item.proto?.cornerRadiusCrop);
+ }
+ return '[empty]';
+ }
+
+ private getRelativeParent() {
+ if (this.item.zOrderRelativeOfId === -1) {
+ return 'none';
+ }
+ return this.item.zOrderRelativeOfId;
+ }
+
+ private formatColor(color: Color) {
+ if (color.isEmpty) {
+ return `[empty], alpha: ${color.a}`;
+ } else {
+ return `(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
+ }
+ }
+
+ private getCalcColor() {
+ if (this.item.color) {
+ return this.formatColor(this.item.color);
+ }
+ return 'no color found';
+ }
+
+ private getReqColor() {
+ const proto = this.item.proto;
+ if (proto?.requestedColor) {
+ return this.formatColor(toColor(proto?.requestedColor));
+ }
+ return 'no color found';
+ }
+
+ private getPixelPropertyValue(propVal: number | undefined) {
+ return `${propVal ? this.formatFloat(propVal) : 0} px`;
+ }
+
+ private getInputTransform() {
+ const inputWindowInfo = this.item.proto?.inputWindowInfo;
+ return Transform.fromProto(inputWindowInfo?.transform, /* position */ null);
+ }
+
+ private getCropTouchRegionWithItem() {
+ if (this.item.proto?.inputWindowInfo?.cropLayerId <= 0) {
+ return 'none';
+ }
+ return this.item.proto?.inputWindowInfo?.cropLayerId;
+ }
+}
+
+type TreeSummary = Array<{key: string; value: string}>;
diff --git a/tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts
new file mode 100644
index 0000000..4424759
--- /dev/null
+++ b/tools/winscope/src/viewers/components/surface_flinger_property_groups_component_test.ts
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+import {Component} from '@angular/core';
+import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
+import {MatDividerModule} from '@angular/material/divider';
+import {MatTooltipModule} from '@angular/material/tooltip';
+import {assertDefined} from 'common/assert_utils';
+import {Color} from 'flickerlib/common';
+import {LayerBuilder} from 'test/unit/layer_builder';
+import {SurfaceFlingerPropertyGroupsComponent} from './surface_flinger_property_groups_component';
+import {TransformMatrixComponent} from './transform_matrix_component';
+
+describe('PropertyGroupsComponent', () => {
+ let fixture: ComponentFixture<TestHostComponent>;
+ let component: TestHostComponent;
+ let htmlElement: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
+ imports: [MatDividerModule, MatTooltipModule],
+ declarations: [
+ TestHostComponent,
+ SurfaceFlingerPropertyGroupsComponent,
+ TransformMatrixComponent,
+ ],
+ schemas: [],
+ }).compileComponents();
+ fixture = TestBed.createComponent(TestHostComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+ fixture.detectChanges();
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('renders verbose flags if available', () => {
+ const layer = new LayerBuilder().setFlags(3).build();
+ component.item = layer;
+ fixture.detectChanges();
+
+ const flags = assertDefined(htmlElement.querySelector('.flags'));
+ expect(flags.innerHTML).toMatch('Flags:.*HIDDEN|OPAQUE \\(0x3\\)');
+ });
+
+ it('renders numeric flags if verbose flags not available', () => {
+ const flags = assertDefined(htmlElement.querySelector('.flags'));
+ expect(flags.innerHTML).toMatch('Flags:.*0');
+ });
+
+ it('displays calculated geometry', () => {
+ const calculatedDiv = assertDefined(htmlElement.querySelector('.geometry .left-column'));
+ expect(calculatedDiv.querySelector('transform-matrix')).toBeTruthy();
+ expect(assertDefined(calculatedDiv.querySelector('.crop')).innerHTML).toContain('[empty]');
+ expect(assertDefined(calculatedDiv.querySelector('.final-bounds')).innerHTML).toContain(
+ '[empty]'
+ );
+ });
+
+ it('displays requested geometry', () => {
+ const layer = new LayerBuilder().setFlags(0).build();
+ layer.proto = {
+ requestedTransform: {
+ dsdx: 0,
+ dsdy: 0,
+ dtdx: 0,
+ dtdy: 0,
+ type: 0,
+ },
+ };
+ component.item = layer;
+ fixture.detectChanges();
+ const requestedDiv = assertDefined(htmlElement.querySelector('.geometry .right-column'));
+ expect(requestedDiv.querySelector('transform-matrix')).toBeTruthy();
+ expect(assertDefined(requestedDiv.querySelector('.crop')).innerHTML).toContain('[empty]');
+ });
+
+ it('displays buffer info', () => {
+ const layer = new LayerBuilder().setFlags(0).build();
+ layer.proto = {
+ destinationFrame: {
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ },
+ };
+ component.item = layer;
+ fixture.detectChanges();
+
+ const sizeDiv = htmlElement.querySelector('.buffer .size');
+ expect(assertDefined(sizeDiv).innerHTML).toContain('[empty]');
+ const currFrameDiv = htmlElement.querySelector('.buffer .frame-number');
+ expect(assertDefined(currFrameDiv).innerHTML).toContain('0');
+ const transformDiv = htmlElement.querySelector('.buffer .transform');
+ expect(assertDefined(transformDiv).innerHTML).toContain('IDENTITY');
+ const destFrameDiv = htmlElement.querySelector('.buffer .dest-frame');
+ expect(assertDefined(destFrameDiv).innerHTML).toContain('left: 0, top: 0, right: 0, bottom: 0');
+ });
+
+ it('displays hierarchy info', () => {
+ const zDiv = htmlElement.querySelector('.hierarchy-info .z-order');
+ expect(assertDefined(zDiv).innerHTML).toContain('0');
+ const relParentDiv = htmlElement.querySelector('.hierarchy-info .rel-parent');
+ expect(assertDefined(relParentDiv).innerHTML).toContain('none');
+ });
+
+ it('displays simple calculated effects', () => {
+ const calculatedDiv = assertDefined(htmlElement.querySelector('.effects .left-column'));
+ expect(assertDefined(calculatedDiv.querySelector('.shadow')).innerHTML).toContain('1 px');
+ expect(assertDefined(calculatedDiv.querySelector('.blur')).innerHTML).toContain('1 px');
+ expect(assertDefined(calculatedDiv.querySelector('.corner-radius')).innerHTML).toContain(
+ '1 px'
+ );
+ });
+
+ it('displays simple requested effects', () => {
+ const layer = new LayerBuilder().setFlags(0).build();
+ layer.proto = {
+ requestedShadowRadius: 1,
+ requestedCornerRadius: 1,
+ };
+ component.item = layer;
+ fixture.detectChanges();
+
+ const calculatedDiv = assertDefined(htmlElement.querySelector('.effects .right-column'));
+ expect(assertDefined(calculatedDiv.querySelector('.shadow')).innerHTML).toContain('1 px');
+ expect(assertDefined(calculatedDiv.querySelector('.corner-radius')).innerHTML).toContain(
+ '1 px'
+ );
+ });
+
+ it('displays empty color and alpha value in effects', () => {
+ const layer = new LayerBuilder().setFlags(0).build();
+ layer.color.a = 1;
+ component.item = layer;
+ fixture.detectChanges();
+
+ const colorDiv = htmlElement.querySelector('.color');
+ expect(assertDefined(colorDiv).innerHTML).toContain('[empty], alpha: 1');
+ });
+
+ it('displays rgba color in effects', () => {
+ const layer = new LayerBuilder().setFlags(0).setColor(new Color(0, 0, 0, 1)).build();
+ component.item = layer;
+ fixture.detectChanges();
+
+ const colorDiv = htmlElement.querySelector('.color');
+ expect(assertDefined(colorDiv).innerHTML).toContain('(0, 0, 0, 1)');
+ });
+
+ it('displays not set message if no inputs present', () => {
+ const noInputsMessage = htmlElement.querySelector('.inputs .left-column');
+ expect(assertDefined(noInputsMessage).innerHTML).toContain('not set');
+ });
+
+ it('displays input window info if available', () => {
+ const layer = new LayerBuilder().setFlags(0).build();
+ layer.proto = {
+ inputWindowInfo: {
+ focusable: false,
+ inputTransform: {
+ dsdx: 0,
+ dsdy: 0,
+ dtdx: 0,
+ dtdy: 0,
+ type: 0,
+ },
+ cropLayerId: 0,
+ replaceTouchableRegionWithCrop: false,
+ },
+ };
+ component.item = layer;
+ fixture.detectChanges();
+
+ expect(htmlElement.querySelector('.inputs .left-column transform-matrix')).toBeTruthy();
+
+ const configDiv = assertDefined(htmlElement.querySelector('.inputs .right-column'));
+ expect(assertDefined(configDiv.querySelector('.focusable')).innerHTML).toContain('false');
+ expect(assertDefined(configDiv.querySelector('.crop-touch-region')).innerHTML).toContain(
+ 'none'
+ );
+ expect(assertDefined(configDiv.querySelector('.replace-touch-region')).innerHTML).toContain(
+ 'false'
+ );
+ });
+
+ @Component({
+ selector: 'host-component',
+ template: ` <surface-flinger-property-groups [item]="item"></surface-flinger-property-groups> `,
+ })
+ class TestHostComponent {
+ item = new LayerBuilder().setFlags(0).build();
+ }
+});
diff --git a/tools/winscope/src/viewers/components/transform_matrix_component.ts b/tools/winscope/src/viewers/components/transform_matrix_component.ts
index 36959cc..b174503 100644
--- a/tools/winscope/src/viewers/components/transform_matrix_component.ts
+++ b/tools/winscope/src/viewers/components/transform_matrix_component.ts
@@ -14,12 +14,12 @@
* limitations under the License.
*/
import {Component, Input} from '@angular/core';
-import {Transform} from 'trace/flickerlib/common';
+import {Transform} from 'flickerlib/common';
@Component({
selector: 'transform-matrix',
template: `
- <div *ngIf="transform" class="matrix" [matTooltip]="transform.getTypeAsString()">
+ <div *ngIf="transform" class="matrix" [matTooltip]="transform.getTypeAsString() ?? ''">
<p class="mat-body-1">
{{ formatFloat(transform.matrix.dsdx) }}
</p>
@@ -57,6 +57,6 @@
],
})
export class TransformMatrixComponent {
- @Input() transform!: Transform;
+ @Input() transform: Transform;
@Input() formatFloat!: (num: number) => number;
}
diff --git a/tools/winscope/src/viewers/components/tree_component.ts b/tools/winscope/src/viewers/components/tree_component.ts
index af45c25..d662ce7 100644
--- a/tools/winscope/src/viewers/components/tree_component.ts
+++ b/tools/winscope/src/viewers/components/tree_component.ts
@@ -34,9 +34,11 @@
<tree-node
*ngIf="item && showNode(item)"
class="node"
+ [id]="'node' + item.stableId"
[class.leaf]="isLeaf(this.item)"
- [class.selected]="isHighlighted(item, highlightedItems)"
+ [class.selected]="isHighlighted(item, highlightedItem)"
[class.clickable]="isClickable()"
+ [class.child-selected]="hasSelectedChild()"
[class.hover]="nodeHover"
[class.childHover]="childHover"
[isAlwaysCollapsed]="isAlwaysCollapsed"
@@ -48,6 +50,7 @@
[isCollapsed]="isAlwaysCollapsed ?? isCollapsed()"
[hasChildren]="hasChildren()"
[isPinned]="isPinned()"
+ [isSelected]="isHighlighted(item, highlightedItem)"
(toggleTreeChange)="toggleTree()"
(click)="onNodeClick($event)"
(expandTreeChange)="expandTree()"
@@ -69,12 +72,12 @@
[isFlattened]="isFlattened"
[useGlobalCollapsedState]="useGlobalCollapsedState"
[initialDepth]="initialDepth + 1"
- [highlightedItems]="highlightedItems"
+ [highlightedItem]="highlightedItem"
[pinnedItems]="pinnedItems"
- (highlightedItemChange)="propagateNewHighlightedItem($event)"
+ [itemsClickable]="itemsClickable"
+ (highlightedChange)="propagateNewHighlightedItem($event)"
(pinnedItemChange)="propagateNewPinnedItem($event)"
(selectedTreeChange)="propagateNewSelectedTree($event)"
- [itemsClickable]="itemsClickable"
(hoverStart)="childHover = true"
(hoverEnd)="childHover = false"></tree-view>
</div>
@@ -94,7 +97,7 @@
@Input() store!: PersistentStore;
@Input() isFlattened? = false;
@Input() initialDepth = 0;
- @Input() highlightedItems: string[] = [];
+ @Input() highlightedItem: string = '';
@Input() pinnedItems?: HierarchyTreeNode[] = [];
@Input() itemsClickable?: boolean;
@Input() useGlobalCollapsedState?: boolean;
@@ -104,7 +107,7 @@
return !item || !item.children || item.children.length === 0;
};
- @Output() highlightedItemChange = new EventEmitter<string>();
+ @Output() highlightedChange = new EventEmitter<string>();
@Output() selectedTreeChange = new EventEmitter<UiTreeNode>();
@Output() pinnedItemChange = new EventEmitter<UiTreeNode>();
@Output() hoverStart = new EventEmitter<void>();
@@ -144,7 +147,7 @@
ngOnChanges() {
if (
this.item instanceof HierarchyTreeNode &&
- UiTreeUtils.isHighlighted(this.item, this.highlightedItems)
+ UiTreeUtils.isHighlighted(this.item, this.highlightedItem)
) {
this.selectedTreeChange.emit(this.item);
}
@@ -167,7 +170,7 @@
event.preventDefault();
this.toggleTree();
} else {
- this.updateHighlightedItems();
+ this.updateHighlightedItem();
}
}
@@ -180,9 +183,9 @@
};
}
- private updateHighlightedItems() {
+ private updateHighlightedItem() {
if (this.item?.stableId) {
- this.highlightedItemChange.emit(`${this.item.stableId}`);
+ this.highlightedChange.emit(`${this.item.stableId}`);
}
}
@@ -194,7 +197,7 @@
}
propagateNewHighlightedItem(newId: string) {
- this.highlightedItemChange.emit(newId);
+ this.highlightedChange.emit(newId);
}
propagateNewPinnedItem(newPinnedItem: UiTreeNode) {
@@ -244,6 +247,18 @@
return (!this.isFlattened || isParentEntryInFlatView) && !this.isLeaf(this.item);
}
+ hasSelectedChild() {
+ if (!this.hasChildren()) {
+ return false;
+ }
+ for (const child of this.item!.children!) {
+ if (child.stableId && this.highlightedItem === child.stableId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private setCollapseValue(isCollapsed: boolean) {
if (this.useGlobalCollapsedState) {
this.store.add(
diff --git a/tools/winscope/src/viewers/components/tree_component_test.ts b/tools/winscope/src/viewers/components/tree_component_test.ts
index 2c568bc..55a5b7c 100644
--- a/tools/winscope/src/viewers/components/tree_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_component_test.ts
@@ -15,60 +15,111 @@
*/
import {Component, NO_ERRORS_SCHEMA, ViewChild} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
+import {assertDefined} from 'common/assert_utils';
import {PersistentStore} from 'common/persistent_store';
import {UiTreeNode} from 'viewers/common/ui_tree_utils';
import {TreeComponent} from './tree_component';
+import {TreeNodeComponent} from './tree_node_component';
describe('TreeComponent', () => {
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let htmlElement: HTMLElement;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
- declarations: [TreeComponent, TestHostComponent],
+ declarations: [TreeComponent, TestHostComponent, TreeNodeComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
- });
-
- beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
+ fixture.detectChanges();
});
it('can be created', () => {
- fixture.detectChanges();
expect(component).toBeTruthy();
});
+ it('shows node', () => {
+ const treeNode = htmlElement.querySelector('tree-node');
+ expect(treeNode).toBeTruthy();
+ });
+
+ it('can identify if a parent node has a selected child', () => {
+ expect(component.treeComponent.hasSelectedChild()).toBeFalse();
+ component.highlightedItem = 'child3';
+ fixture.detectChanges();
+ expect(component.treeComponent.hasSelectedChild()).toBeTrue();
+ });
+
+ it('highlights item upon node click', () => {
+ const treeNode = htmlElement.querySelector('tree-node');
+ expect(treeNode).toBeTruthy();
+
+ const spy = spyOn(component.treeComponent.highlightedChange, 'emit');
+ (treeNode as HTMLButtonElement).dispatchEvent(new MouseEvent('click', {detail: 1}));
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('toggles tree upon node double click', () => {
+ const treeNode = htmlElement.querySelector('tree-node');
+ expect(treeNode).toBeTruthy();
+
+ const currCollapseValue = component.treeComponent.localCollapsedState;
+ (treeNode as HTMLButtonElement).dispatchEvent(new MouseEvent('click', {detail: 2}));
+ fixture.detectChanges();
+ expect(!currCollapseValue).toBe(component.treeComponent.localCollapsedState);
+ });
+
+ it('scrolls selected node into view if out of view', async () => {
+ const treeNode = assertDefined(htmlElement.querySelector(`#nodechild50`));
+ const spy = spyOn(treeNode, 'scrollIntoView');
+ component.highlightedItem = 'child50';
+ fixture.detectChanges();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('does not scroll selected element if already in view', () => {
+ const treeNode = assertDefined(htmlElement.querySelector(`#nodechild2`));
+ const spy = spyOn(treeNode, 'scrollIntoView');
+ component.highlightedItem = 'child2';
+ fixture.detectChanges();
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ function makeTreeNodeChildren(): UiTreeNode[] {
+ const children = [];
+ for (let i = 0; i < 60; i++) {
+ children.push({kind: `${i}`, stableId: `child${i}`, name: `Child${i}`});
+ }
+ return children;
+ }
+
@Component({
selector: 'host-component',
template: `
<tree-view
[item]="item"
[store]="store"
- [isFlattened]="isFlattened"
- [diffClass]="diffClass"
- [isHighlighted]="isHighlighted"
- [hasChildren]="hasChildren"></tree-view>
+ [isFlattened]="false"
+ [isPinned]="false"
+ [highlightedItem]="highlightedItem"
+ [itemsClickable]="true"></tree-view>
`,
})
class TestHostComponent {
- isFlattened = true;
item: UiTreeNode = {
simplifyNames: false,
kind: 'entry',
name: 'LayerTraceEntry',
- shortName: 'LTE',
- chips: [],
- children: [{kind: '3', stableId: '3', name: 'Child1'}],
+ stableId: 'LayerTraceEntry 2',
+ children: makeTreeNodeChildren(),
};
store = new PersistentStore();
- diffClass = jasmine.createSpy().and.returnValue('none');
- isHighlighted = jasmine.createSpy().and.returnValue(false);
- hasChildren = jasmine.createSpy().and.returnValue(true);
+ highlightedItem = '';
@ViewChild(TreeComponent)
treeComponent!: TreeComponent;
diff --git a/tools/winscope/src/viewers/components/tree_node_component.ts b/tools/winscope/src/viewers/components/tree_node_component.ts
index fc88c36..a2e3e6b 100644
--- a/tools/winscope/src/viewers/components/tree_node_component.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {Component, ElementRef, EventEmitter, Inject, Input, Output} from '@angular/core';
import {DiffType, HierarchyTreeNode, UiTreeNode, UiTreeUtils} from 'viewers/common/ui_tree_utils';
import {nodeInnerItemStyles} from 'viewers/components/styles/node.styles';
@@ -62,15 +62,34 @@
@Input() isPinned?: boolean = false;
@Input() isInPinnedSection?: boolean = false;
@Input() isAlwaysCollapsed?: boolean;
+ @Input() isSelected?: boolean = false;
@Output() toggleTreeChange = new EventEmitter<void>();
@Output() expandTreeChange = new EventEmitter<boolean>();
@Output() pinNodeChange = new EventEmitter<UiTreeNode>();
collapseDiffClass = '';
+ private el: HTMLElement;
+
+ constructor(@Inject(ElementRef) public elementRef: ElementRef) {
+ this.el = elementRef.nativeElement;
+ }
ngOnChanges() {
this.collapseDiffClass = this.updateCollapseDiffClass();
+ if (!this.isPinned && this.isSelected && !this.isNodeInView()) {
+ this.el.scrollIntoView({block: 'center', inline: 'nearest'});
+ }
+ }
+
+ isNodeInView() {
+ const rect = this.el.getBoundingClientRect();
+ return (
+ rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+ );
}
isPropertiesTreeNode() {
diff --git a/tools/winscope/src/viewers/components/tree_node_component_test.ts b/tools/winscope/src/viewers/components/tree_node_component_test.ts
index 3854f7d..c80e697 100644
--- a/tools/winscope/src/viewers/components/tree_node_component_test.ts
+++ b/tools/winscope/src/viewers/components/tree_node_component_test.ts
@@ -15,52 +15,91 @@
*/
import {Component, NO_ERRORS_SCHEMA, ViewChild} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
+import {MatIconModule} from '@angular/material/icon';
import {TreeNodeComponent} from './tree_node_component';
+import {TreeNodeDataViewComponent} from './tree_node_data_view_component';
describe('TreeNodeComponent', () => {
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let htmlElement: HTMLElement;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
+ imports: [MatIconModule],
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
- declarations: [TreeNodeComponent, TestHostComponent],
+ declarations: [TreeNodeComponent, TreeNodeDataViewComponent, TestHostComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
- });
-
- beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
+ fixture.detectChanges();
});
it('can be created', () => {
- fixture.detectChanges();
expect(component).toBeTruthy();
});
+ it('can generate data view component', () => {
+ component.treeNodeComponent.isPropertiesTreeNode = jasmine.createSpy().and.returnValue(false);
+ fixture.detectChanges();
+ const treeNodeDataView = htmlElement.querySelector('tree-node-data-view');
+ expect(treeNodeDataView).toBeTruthy();
+ });
+
+ it('can trigger tree toggle on click of chevron', () => {
+ component.treeNodeComponent.showChevron = jasmine.createSpy().and.returnValue(true);
+ fixture.detectChanges();
+
+ const spy = spyOn(component.treeNodeComponent.toggleTreeChange, 'emit');
+ const toggleButton = htmlElement.querySelector('.toggle-tree-btn');
+ expect(toggleButton).toBeTruthy();
+ (toggleButton as HTMLButtonElement).click();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('can trigger tree expansion on click of expand tree button', () => {
+ const spy = spyOn(component.treeNodeComponent.expandTreeChange, 'emit');
+ const expandButton = htmlElement.querySelector('.expand-tree-btn');
+ expect(expandButton).toBeTruthy();
+ (expandButton as HTMLButtonElement).click();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('can trigger node pin on click of star', () => {
+ component.treeNodeComponent.showPinNodeIcon = jasmine.createSpy().and.returnValue(true);
+ fixture.detectChanges();
+
+ const spy = spyOn(component.treeNodeComponent.pinNodeChange, 'emit');
+ const pinNodeButton = htmlElement.querySelector('.pin-node-btn');
+ expect(pinNodeButton).toBeTruthy();
+ (pinNodeButton as HTMLButtonElement).click();
+ expect(spy).toHaveBeenCalledWith(component.item);
+ });
+
@Component({
selector: 'host-component',
template: `
<tree-node
[item]="item"
- [isCollapsed]="true"
+ [isCollapsed]="false"
[isPinned]="false"
[isInPinnedSection]="false"
- [hasChildren]="false"></tree-node>
+ [hasChildren]="true"
+ [isSelected]="isSelected"></tree-node>
`,
})
class TestHostComponent {
item = {
- simplifyNames: false,
kind: 'entry',
name: 'LayerTraceEntry',
- shortName: 'LTE',
- chips: [],
+ stableId: '4',
+ children: [{stableId: 'child'}],
};
+ isSelected = false;
+
@ViewChild(TreeNodeComponent)
treeNodeComponent!: TreeNodeComponent;
}
diff --git a/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts b/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts
new file mode 100644
index 0000000..36a8a74
--- /dev/null
+++ b/tools/winscope/src/viewers/components/view_capture_property_groups_component.ts
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+import {Component, Input} from '@angular/core';
+import {ViewNode} from 'trace/trace_type';
+
+@Component({
+ selector: 'view-capture-property-groups',
+ template: `
+ <div class="group">
+ <h3 class="group-header mat-subheading-2">View</h3>
+ <div class="left-column">
+ <p class="mat-body-1 flags">
+ <span class="mat-body-2">Class: </span>
+ &ngsp;
+ {{ item.className }}
+ </p>
+ <p class="mat-body-1 flags">
+ <span class="mat-body-2">Hashcode: </span>
+ &ngsp;
+ {{ item.hashcode }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group">
+ <h3 class="group-header mat-subheading-2">Geometry</h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Coordinates</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Left: </span>
+ &ngsp;
+ {{ item.left }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Top: </span>
+ &ngsp;
+ {{ item.top }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Elevation: </span>
+ &ngsp;
+ {{ item.elevation }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Size</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Height: </span>
+ &ngsp;
+ {{ item.height }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Width: </span>
+ &ngsp;
+ {{ item.width }}
+ </p>
+ </div>
+ </div>
+ <div class="group">
+ <h3 class="group-header mat-subheading-2"></h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Translation</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Translation X: </span>
+ &ngsp;
+ {{ item.translationX }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Translation Y: </span>
+ &ngsp;
+ {{ item.translationY }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Scroll</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Scroll X: </span>
+ &ngsp;
+ {{ item.scrollX }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Scroll Y: </span>
+ &ngsp;
+ {{ item.scrollY }}
+ </p>
+ </div>
+ </div>
+ <div class="group">
+ <h3 class="group-header mat-subheading-2"></h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Scale</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Scale X: </span>
+ &ngsp;
+ {{ item.scaleX }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Scale Y: </span>
+ &ngsp;
+ {{ item.scaleY }}
+ </p>
+ </div>
+ </div>
+ <mat-divider></mat-divider>
+ <div class="group">
+ <h3 class="group-header mat-subheading-2">Effects</h3>
+ <div class="left-column">
+ <p class="column-header mat-small">Translation</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Visibility: </span>
+ &ngsp;
+ {{ item.visibility }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Alpha: </span>
+ &ngsp;
+ {{ item.alpha }}
+ </p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Will Not Draw: </span>
+ &ngsp;
+ {{ item.willNotDraw }}
+ </p>
+ </div>
+ <div class="right-column">
+ <p class="column-header mat-small">Miscellaneous</p>
+ <p class="mat-body-1">
+ <span class="mat-body-2">Clip Children: </span>
+ &ngsp;
+ {{ item.clipChildren }}
+ </p>
+ </div>
+ </div>
+ `,
+ styles: [
+ `
+ .group {
+ display: flex;
+ flex-direction: row;
+ padding: 8px;
+ }
+
+ .group-header {
+ width: 80px;
+ color: gray;
+ }
+
+ .left-column {
+ flex: 1;
+ padding: 0 5px;
+ }
+
+ .right-column {
+ flex: 1;
+ border: 1px solid var(--border-color);
+ border-left-width: 5px;
+ padding: 0 5px;
+ }
+
+ .column-header {
+ color: gray;
+ }
+ `,
+ ],
+})
+export class ViewCapturePropertyGroupsComponent {
+ @Input() item: ViewNode;
+}
diff --git a/tools/winscope/src/viewers/components/viewer_input_method_component.ts b/tools/winscope/src/viewers/components/viewer_input_method_component.ts
index d51813f..574d791 100644
--- a/tools/winscope/src/viewers/components/viewer_input_method_component.ts
+++ b/tools/winscope/src/viewers/components/viewer_input_method_component.ts
@@ -28,7 +28,7 @@
class="hierarchy-view"
[tree]="inputData?.tree ?? null"
[dependencies]="inputData?.dependencies ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [highlightedItem]="inputData?.highlightedItem"
[pinnedItems]="inputData?.pinnedItems ?? []"
[tableProperties]="inputData?.hierarchyTableProperties"
[store]="store"
@@ -39,6 +39,7 @@
<ime-additional-properties
class="ime-additional-properties"
+ [highlightedItem]="inputData?.highlightedItem"
[additionalProperties]="inputData?.additionalProperties!"></ime-additional-properties>
</ng-container>
</div>
diff --git a/tools/winscope/src/viewers/viewer.ts b/tools/winscope/src/viewers/viewer.ts
index c6ccb7b..b6bbe7c 100644
--- a/tools/winscope/src/viewers/viewer.ts
+++ b/tools/winscope/src/viewers/viewer.ts
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import {TracePosition} from 'trace/trace_position';
+import {WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent, WinscopeEventEmitter} from 'messaging/winscope_event_emitter';
+import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {TraceType} from 'trace/trace_type';
enum ViewType {
@@ -32,8 +33,9 @@
) {}
}
-interface Viewer {
- onTracePositionUpdate(position: TracePosition): Promise<void>;
+interface Viewer extends WinscopeEventListener, WinscopeEventEmitter {
+ onWinscopeEvent(event: WinscopeEvent): Promise<void>;
+ setEmitEvent(callback: EmitEvent): void;
getViews(): View[];
getDependencies(): TraceType[];
}
diff --git a/tools/winscope/src/viewers/viewer_factory.ts b/tools/winscope/src/viewers/viewer_factory.ts
index 001302f..dae30ca 100644
--- a/tools/winscope/src/viewers/viewer_factory.ts
+++ b/tools/winscope/src/viewers/viewer_factory.ts
@@ -25,7 +25,11 @@
import {ViewerSurfaceFlinger} from './viewer_surface_flinger/viewer_surface_flinger';
import {ViewerTransactions} from './viewer_transactions/viewer_transactions';
import {ViewerTransitions} from './viewer_transitions/viewer_transitions';
-import {ViewerViewCapture} from './viewer_view_capture/viewer_view_capture';
+import {
+ ViewerViewCaptureLauncherActivity,
+ ViewerViewCaptureTaskbarDragLayer,
+ ViewerViewCaptureTaskbarOverlayDragLayer,
+} from './viewer_view_capture/viewer_view_capture';
import {ViewerWindowManager} from './viewer_window_manager/viewer_window_manager';
class ViewerFactory {
@@ -42,10 +46,13 @@
ViewerProtoLog,
ViewerScreenRecording,
ViewerTransitions,
- ViewerViewCapture,
+ ViewerViewCaptureLauncherActivity,
+ ViewerViewCaptureTaskbarDragLayer,
+ ViewerViewCaptureTaskbarOverlayDragLayer,
];
- createViewers(activeTraceTypes: Set<TraceType>, traces: Traces, storage: Storage): Viewer[] {
+ createViewers(traces: Traces, storage: Storage): Viewer[] {
+ const activeTraceTypes = new Set(traces.mapTrace((trace) => trace.type));
const viewers: Viewer[] = [];
for (const Viewer of ViewerFactory.VIEWERS) {
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
index 87d5fb5..f24b80d 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
@@ -16,11 +16,11 @@
import {ArrayUtils} from 'common/array_utils';
import {assertDefined} from 'common/assert_utils';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {LogMessage} from 'trace/protolog';
import {Trace, TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {UiData, UiDataMessage} from './ui_data';
@@ -48,11 +48,13 @@
this.notifyUiDataCallback(this.uiData);
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.initializeIfNeeded();
- this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
- this.computeUiDataCurrentMessageIndex();
- this.notifyUiDataCallback(this.uiData);
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.initializeIfNeeded();
+ this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+ this.computeUiDataCurrentMessageIndex();
+ this.notifyUiDataCallback(this.uiData);
+ });
}
onLogLevelsFilterChanged(levels: string[]) {
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
index a2de977..23ab4d8 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -15,13 +15,13 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp} from 'common/time';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {TracesBuilder} from 'test/unit/traces_builder';
import {TraceBuilder} from 'test/unit/trace_builder';
import {LogMessage} from 'trace/protolog';
-import {RealTimestamp} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {Presenter} from './presenter';
import {UiData, UiDataMessage} from './ui_data';
@@ -30,9 +30,9 @@
let presenter: Presenter;
let inputMessages: UiDataMessage[];
let trace: Trace<LogMessage>;
- let position10: TracePosition;
- let position11: TracePosition;
- let position12: TracePosition;
+ let positionUpdate10: TracePositionUpdate;
+ let positionUpdate11: TracePositionUpdate;
+ let positionUpdate12: TracePositionUpdate;
let outputUiData: undefined | UiData;
beforeEach(async () => {
@@ -54,9 +54,9 @@
.setTimestamps([time10, time11, time12])
.build();
- position10 = TracePosition.fromTimestamp(time10);
- position11 = TracePosition.fromTimestamp(time11);
- position12 = TracePosition.fromTimestamp(time12);
+ positionUpdate10 = TracePositionUpdate.fromTimestamp(time10);
+ positionUpdate11 = TracePositionUpdate.fromTimestamp(time11);
+ positionUpdate12 = TracePositionUpdate.fromTimestamp(time12);
outputUiData = undefined;
@@ -65,7 +65,7 @@
presenter = new Presenter(traces, (data: UiData) => {
outputUiData = data;
});
- await presenter.onTracePositionUpdate(position10); // trigger initialization
+ await presenter.onAppEvent(positionUpdate10); // trigger initialization
});
it('is robust to empty trace', async () => {
@@ -77,13 +77,13 @@
expect(assertDefined(outputUiData).messages).toEqual([]);
expect(assertDefined(outputUiData).currentMessageIndex).toBeUndefined();
- await presenter.onTracePositionUpdate(position10);
+ await presenter.onAppEvent(positionUpdate10);
expect(assertDefined(outputUiData).messages).toEqual([]);
expect(assertDefined(outputUiData).currentMessageIndex).toBeUndefined();
});
it('processes trace position updates', async () => {
- await presenter.onTracePositionUpdate(position10);
+ await presenter.onAppEvent(positionUpdate10);
expect(assertDefined(outputUiData).allLogLevels).toEqual(['level0', 'level1', 'level2']);
expect(assertDefined(outputUiData).allTags).toEqual(['tag0', 'tag1', 'tag2']);
@@ -153,7 +153,7 @@
it('computes current message index', async () => {
// Position -> entry #0
- await presenter.onTracePositionUpdate(position10);
+ await presenter.onAppEvent(positionUpdate10);
presenter.onLogLevelsFilterChanged([]);
expect(assertDefined(outputUiData).currentMessageIndex).toEqual(0);
@@ -164,7 +164,7 @@
expect(assertDefined(outputUiData).currentMessageIndex).toEqual(0);
// Position -> entry #1
- await presenter.onTracePositionUpdate(position11);
+ await presenter.onAppEvent(positionUpdate11);
presenter.onLogLevelsFilterChanged([]);
expect(assertDefined(outputUiData).currentMessageIndex).toEqual(1);
@@ -178,7 +178,7 @@
expect(assertDefined(outputUiData).currentMessageIndex).toEqual(1);
// Position -> entry #2
- await presenter.onTracePositionUpdate(position12);
+ await presenter.onAppEvent(positionUpdate12);
presenter.onLogLevelsFilterChanged([]);
expect(assertDefined(outputUiData).currentMessageIndex).toEqual(2);
});
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
index 5a6e3c4..6156b2f 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {WinscopeEvent} from 'messaging/winscope_event';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
@@ -44,8 +44,12 @@
});
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent() {
+ // do nothing
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
index 7a05d5f..44beae8 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
@@ -55,7 +55,7 @@
</mat-form-field>
</div>
<div class="text">
- <mat-form-field appearance="fill">
+ <mat-form-field appearance="fill" (keydown.enter)="$event.target.blur()">
<mat-label>Search text</mat-label>
<input matInput [(ngModel)]="searchString" (input)="onSearchStringChange()" />
</mat-form-field>
diff --git a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
index 65cfc33..ec130a3 100644
--- a/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
+++ b/tools/winscope/src/viewers/viewer_screen_recording/viewer_screen_recording.ts
@@ -15,11 +15,11 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {ViewerScreenRecordingComponent} from './viewer_screen_recording_component';
@@ -34,10 +34,16 @@
this.htmlElement = document.createElement('viewer-screen-recording');
}
- async onTracePositionUpdate(position: TracePosition) {
- const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
- (this.htmlElement as unknown as ViewerScreenRecordingComponent).currentTraceEntry =
- await entry?.getValue();
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+ (this.htmlElement as unknown as ViewerScreenRecordingComponent).currentTraceEntry =
+ await entry?.getValue();
+ });
+ }
+
+ setEmitEvent() {
+ // do nothing
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_stub.ts b/tools/winscope/src/viewers/viewer_stub.ts
index d5f76e2..9e47da3 100644
--- a/tools/winscope/src/viewers/viewer_stub.ts
+++ b/tools/winscope/src/viewers/viewer_stub.ts
@@ -14,12 +14,20 @@
* limitations under the License.
*/
-import {TracePosition} from 'trace/trace_position';
+import {FunctionUtils} from 'common/function_utils';
+import {WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {TraceType} from 'trace/trace_type';
import {View, Viewer, ViewType} from './viewer';
class ViewerStub implements Viewer {
- constructor(title: string, viewContent?: string) {
+ private htmlElement: HTMLElement;
+ private title: string;
+ private view: View;
+ private dependencies: TraceType[];
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
+
+ constructor(title: string, viewContent?: string, dependencies?: TraceType[]) {
this.title = title;
if (viewContent !== undefined) {
@@ -28,30 +36,37 @@
} else {
this.htmlElement = undefined as unknown as HTMLElement;
}
+
+ this.dependencies = dependencies ?? [TraceType.WINDOW_MANAGER];
+
+ this.view = new View(
+ ViewType.TAB,
+ this.getDependencies(),
+ this.htmlElement,
+ this.title,
+ this.getDependencies()[0]
+ );
}
- onTracePositionUpdate(position: TracePosition): Promise<void> {
+ onWinscopeEvent(event: WinscopeEvent): Promise<void> {
return Promise.resolve();
}
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ async emitAppEventForTesting(event: WinscopeEvent) {
+ await this.emitAppEvent(event);
+ }
+
getViews(): View[] {
- return [
- new View(
- ViewType.TAB,
- this.getDependencies(),
- this.htmlElement,
- this.title,
- this.getDependencies()[0]
- ),
- ];
+ return [this.view];
}
getDependencies(): TraceType[] {
- return [TraceType.WINDOW_MANAGER];
+ return this.dependencies;
}
-
- private htmlElement: HTMLElement;
- private title: string;
}
export {ViewerStub};
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
index 49594fe..9d8f8ea 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
@@ -17,30 +17,33 @@
import {assertDefined} from 'common/assert_utils';
import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {FilterType, TreeUtils} from 'common/tree_utils';
-import {Layer} from 'trace/flickerlib/layers/Layer';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
+import {Layer} from 'flickerlib/layers/Layer';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
-import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle';
+import {SurfaceFlingerUtils} from 'viewers/common/surface_flinger_utils';
import {TreeGenerator} from 'viewers/common/tree_generator';
import {TreeTransformer} from 'viewers/common/tree_transformer';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {ViewCaptureUtils} from 'viewers/common/view_capture_utils';
import {UiData} from './ui_data';
type NotifyViewCallbackType = (uiData: UiData) => void;
export class Presenter {
private readonly notifyViewCallback: NotifyViewCallbackType;
+ private readonly traces: Traces;
private readonly trace: Trace<LayerTraceEntry>;
+ private viewCapturePackageNames: string[] = [];
private uiData: UiData;
private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
- private highlightedItems: string[] = [];
- private displayIds: number[] = [];
+ private highlightedItem: string = '';
+ private highlightedProperty: string = '';
private pinnedItems: HierarchyTreeNode[] = [];
private pinnedIds: string[] = [];
private selectedHierarchyTree: HierarchyTreeNode | null = null;
@@ -53,6 +56,7 @@
showDiff: {
name: 'Show diff', // TODO: PersistentStoreObject.Ignored("Show diff") or something like that to instruct to not store this info
enabled: false,
+ isUnavailable: false,
},
simplifyNames: {
name: 'Simplify names',
@@ -76,6 +80,7 @@
showDiff: {
name: 'Show diff',
enabled: false,
+ isUnavailable: false,
},
showDefaults: {
name: 'Show defaults',
@@ -95,31 +100,47 @@
private readonly storage: Storage,
notifyViewCallback: NotifyViewCallbackType
) {
+ this.traces = traces;
this.trace = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER));
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData([TraceType.SURFACE_FLINGER]);
this.copyUiDataAndNotifyView();
}
- async onTracePositionUpdate(position: TracePosition) {
- this.uiData = new UiData();
- this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
- this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.initializeIfNeeded();
- const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
- const prevEntry =
- entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
+ const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+ const prevEntry =
+ entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
- this.entry = (await entry?.getValue()) ?? null;
- this.previousEntry = (await prevEntry?.getValue()) ?? null;
- if (this.entry) {
- this.uiData.highlightedItems = this.highlightedItems;
- this.uiData.rects = this.generateRects();
- this.uiData.displayIds = this.displayIds;
- this.uiData.tree = this.generateTree();
- }
+ this.entry = (await entry?.getValue()) ?? null;
+ this.previousEntry = (await prevEntry?.getValue()) ?? null;
+ if (this.hierarchyUserOptions['showDiff'].isUnavailable !== undefined) {
+ this.hierarchyUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+ }
+ if (this.propertiesUserOptions['showDiff'].isUnavailable !== undefined) {
+ this.propertiesUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+ }
- this.copyUiDataAndNotifyView();
+ this.uiData = new UiData();
+ this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+ this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+
+ if (this.entry) {
+ this.uiData.highlightedItem = this.highlightedItem;
+ this.uiData.highlightedProperty = this.highlightedProperty;
+ this.uiData.rects = SurfaceFlingerUtils.makeRects(
+ this.entry,
+ this.viewCapturePackageNames,
+ this.hierarchyUserOptions
+ );
+ this.uiData.displayIds = this.getDisplayIds(this.entry);
+ this.uiData.tree = this.generateTree();
+ }
+ this.copyUiDataAndNotifyView();
+ });
}
updatePinnedItems(pinnedItem: HierarchyTreeNode) {
@@ -134,14 +155,23 @@
this.copyUiDataAndNotifyView();
}
- updateHighlightedItems(id: string) {
- if (this.highlightedItems.includes(id)) {
- this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
+ updateHighlightedItem(id: string) {
+ if (this.highlightedItem === id) {
+ this.highlightedItem = '';
} else {
- this.highlightedItems = []; //if multi-select surfaces implemented, remove this line
- this.highlightedItems.push(id);
+ this.highlightedItem = id;
}
- this.uiData.highlightedItems = this.highlightedItems;
+ this.uiData.highlightedItem = this.highlightedItem;
+ this.copyUiDataAndNotifyView();
+ }
+
+ updateHighlightedProperty(id: string) {
+ if (this.highlightedProperty === id) {
+ this.highlightedProperty = '';
+ } else {
+ this.highlightedProperty = id;
+ }
+ this.uiData.highlightedProperty = this.highlightedProperty;
this.copyUiDataAndNotifyView();
}
@@ -174,62 +204,21 @@
this.updateSelectedTreeUiData();
}
- private generateRects(): Rectangle[] {
- const displayRects =
- this.entry.displays.map((display: any) => {
- const rect = display.layerStackSpace;
- rect.label = 'Display';
- if (display.name) {
- rect.label += ` - ${display.name}`;
- }
- rect.stableId = `Display - ${display.id}`;
- rect.displayId = display.layerStackId;
- rect.isDisplay = true;
- rect.cornerRadius = 0;
- rect.isVirtual = display.isVirtual ?? false;
- rect.transform = {
- matrix: display.transform.matrix,
- };
- return rect;
- }) ?? [];
- this.displayIds = this.entry.displays.map((it: any) => it.layerStackId);
- this.displayIds.sort();
- const rects = this.getLayersForRectsView()
- .sort(this.compareLayerZ)
- .map((it: any) => {
- const rect = it.rect;
- rect.displayId = it.stackId;
- rect.cornerRadius = it.cornerRadius;
- if (!this.displayIds.includes(it.stackId)) {
- this.displayIds.push(it.stackId);
- }
- rect.transform = {
- matrix: rect.transform.matrix,
- };
- return rect;
- });
-
- return this.rectsToUiData(rects.concat(displayRects));
+ private async initializeIfNeeded() {
+ this.viewCapturePackageNames = await ViewCaptureUtils.getPackageNames(this.traces);
}
- private getLayersForRectsView(): Layer[] {
- const onlyVisible = this.hierarchyUserOptions['onlyVisible']?.enabled ?? false;
- // Show only visible layers or Visible + Occluded layers. Don't show all layers
- // (flattenedLayers) because container layers are never meant to be displayed
- return this.entry.flattenedLayers.filter(
- (it: any) => it.isVisible || (!onlyVisible && it.occludedBy.length > 0)
- );
- }
-
- private compareLayerZ(a: Layer, b: Layer): number {
- const zipLength = Math.min(a.zOrderPath.length, b.zOrderPath.length);
- for (let i = 0; i < zipLength; ++i) {
- const zOrderA = a.zOrderPath[i];
- const zOrderB = b.zOrderPath[i];
- if (zOrderA > zOrderB) return -1;
- if (zOrderA < zOrderB) return 1;
- }
- return b.zOrderPath.length - a.zOrderPath.length;
+ private getDisplayIds(entry: LayerTraceEntry): number[] {
+ const ids = new Set<number>();
+ entry.displays.forEach((display: any) => {
+ ids.add(display.layerStackId);
+ });
+ entry.flattenedLayers.forEach((layer: Layer) => {
+ ids.add(layer.stackId);
+ });
+ return Array.from(ids.values()).sort((a, b) => {
+ return a - b;
+ });
}
private updateSelectedTreeUiData() {
@@ -254,7 +243,10 @@
.setIsFlatView(this.hierarchyUserOptions['flat']?.enabled)
.withUniqueNodeId();
let tree: HierarchyTreeNode | null;
- if (!this.hierarchyUserOptions['showDiff']?.enabled) {
+ if (
+ !this.hierarchyUserOptions['showDiff']?.enabled ||
+ this.hierarchyUserOptions['showDiff']?.isUnavailable
+ ) {
tree = generator.generateTree();
} else {
tree = generator
@@ -267,49 +259,6 @@
return tree;
}
- private rectsToUiData(rects: any[]): Rectangle[] {
- const uiRects: Rectangle[] = [];
- rects.forEach((rect: any) => {
- let t = null;
- if (rect.transform && rect.transform.matrix) {
- t = rect.transform.matrix;
- } else if (rect.transform) {
- t = rect.transform;
- }
- let transform: RectTransform | null = null;
- if (t !== null) {
- const matrix: RectMatrix = {
- dsdx: t.dsdx,
- dsdy: t.dsdy,
- dtdx: t.dtdx,
- dtdy: t.dtdy,
- tx: t.tx,
- ty: t.ty,
- };
- transform = {
- matrix,
- };
- }
-
- const newRect: Rectangle = {
- topLeft: {x: rect.left, y: rect.top},
- bottomRight: {x: rect.right, y: rect.bottom},
- label: rect.label,
- transform,
- isVisible: rect.ref?.isVisible ?? false,
- isDisplay: rect.isDisplay ?? false,
- ref: rect.ref,
- id: rect.stableId ?? rect.ref.stableId,
- displayId: rect.displayId ?? rect.ref.stackId,
- isVirtual: rect.isVirtual ?? false,
- isClickable: !(rect.isDisplay ?? false),
- cornerRadius: rect.cornerRadius,
- };
- uiRects.push(newRect);
- });
- return uiRects;
- }
-
private updatePinnedIds(newId: string) {
if (this.pinnedIds.includes(newId)) {
this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
@@ -322,7 +271,10 @@
const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
.setOnlyProtoDump(true)
.setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
- .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
+ .setIsShowDiff(
+ this.propertiesUserOptions['showDiff']?.enabled &&
+ !this.propertiesUserOptions['showDiff']?.isUnavailable
+ )
.setTransformerOptions({skip: selectedTree.skip})
.setProperties(this.entry)
.setDiffProperties(this.previousEntry);
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
index 2cf363f..cb5531a 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
@@ -14,15 +14,15 @@
* limitations under the License.
*/
+import {RealTimestamp} from 'common/time';
+import {LayerTraceEntry} from 'flickerlib/layers/LayerTraceEntry';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
import {MockStorage} from 'test/unit/mock_storage';
import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {LayerTraceEntry} from 'trace/flickerlib/layers/LayerTraceEntry';
-import {RealTimestamp} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
@@ -31,8 +31,8 @@
describe('PresenterSurfaceFlinger', () => {
let trace: Trace<LayerTraceEntry>;
- let position: TracePosition;
- let positionMultiDisplayEntry: TracePosition;
+ let positionUpdate: TracePositionUpdate;
+ let positionUpdateMultiDisplayEntry: TracePositionUpdate;
let presenter: Presenter;
let uiData: UiData;
let selectedTree: HierarchyTreeNode;
@@ -45,15 +45,14 @@
])
.build();
- position = TracePosition.fromTraceEntry(trace.getEntry(0));
- positionMultiDisplayEntry = TracePosition.fromTraceEntry(trace.getEntry(1));
+ positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
+ positionUpdateMultiDisplayEntry = TracePositionUpdate.fromTraceEntry(trace.getEntry(1));
selectedTree = new HierarchyTreeBuilder()
.setName('Dim layer#53')
- .setStableId('EffectLayer 53 Dim layer#53')
+ .setStableId('53 Dim layer#53')
.setFilteredView(true)
.setKind('53')
- .setDiffType('EffectLayer')
.setId(53)
.build();
});
@@ -66,17 +65,19 @@
const emptyTrace = new TraceBuilder<LayerTraceEntry>().setEntries([]).build();
const presenter = createPresenter(emptyTrace);
- const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
- await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+ const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+ new RealTimestamp(0n)
+ );
+ await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
});
it('processes trace position updates', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.rects.length).toBeGreaterThan(0);
- expect(uiData.highlightedItems?.length).toEqual(0);
+ expect(uiData.highlightedItem?.length).toEqual(0);
expect(uiData.displayIds).toContain(0);
const hierarchyOpts = uiData.hierarchyUserOptions
? Object.keys(uiData.hierarchyUserOptions)
@@ -89,11 +90,27 @@
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
});
+ it('disables show diff and generates non-diff tree if no prev entry available', async () => {
+ await presenter.onAppEvent(positionUpdate);
+
+ const hierarchyOpts = uiData.hierarchyUserOptions ?? null;
+ expect(hierarchyOpts).toBeTruthy();
+ expect(hierarchyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+ const propertyOpts = uiData.propertiesUserOptions ?? null;
+ expect(propertyOpts).toBeTruthy();
+ expect(propertyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+ expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
+ });
+
it('creates input data for rects view', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.rects.length).toBeGreaterThan(0);
- expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 0});
- expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 118});
+ expect(uiData.rects[0].x).toEqual(0);
+ expect(uiData.rects[0].y).toEqual(0);
+ expect(uiData.rects[0].w).toEqual(1080);
+ expect(uiData.rects[0].h).toEqual(74);
});
it('updates pinned items', () => {
@@ -108,12 +125,20 @@
expect(uiData.pinnedItems).toContain(pinnedItem);
});
- it('updates highlighted items', () => {
- expect(uiData.highlightedItems).toEqual([]);
+ it('updates highlighted item', () => {
+ expect(uiData.highlightedItem).toEqual('');
const id = '4';
- presenter.updateHighlightedItems(id);
- expect(uiData.highlightedItems).toContain(id);
+ presenter.updateHighlightedItem(id);
+ expect(uiData.highlightedItem).toBe(id);
+ });
+
+ it('updates highlighted property', () => {
+ expect(uiData.highlightedProperty).toEqual('');
+
+ const id = '4';
+ presenter.updateHighlightedProperty(id);
+ expect(uiData.highlightedProperty).toBe(id);
});
it('updates hierarchy tree', async () => {
@@ -137,7 +162,7 @@
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.tree?.children.length).toEqual(3);
presenter.updateHierarchyTree(userOptions);
@@ -165,7 +190,7 @@
enabled: true,
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.updateHierarchyTree(userOptions);
expect(uiData.tree?.children.length).toEqual(94);
presenter.filterHierarchyTree('Wallpaper');
@@ -174,7 +199,7 @@
});
it('sets properties tree and associated ui data', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
// does not check specific tree values as tree transformation method may change
expect(uiData.propertiesTree).toBeTruthy();
@@ -198,7 +223,7 @@
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
expect(uiData.propertiesTree?.diffType).toBeFalsy();
@@ -208,7 +233,7 @@
});
it('filters properties tree', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
let nonTerminalChildren =
uiData.propertiesTree?.children?.filter(
@@ -226,7 +251,7 @@
});
it('handles displays with no visible layers', async () => {
- await presenter.onTracePositionUpdate(positionMultiDisplayEntry);
+ await presenter.onAppEvent(positionUpdateMultiDisplayEntry);
expect(uiData.displayIds.length).toEqual(5);
// we want the ids to be sorted
expect(uiData.displayIds).toEqual([0, 2, 3, 4, 5]);
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts b/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
index 319bf96..36fe0ae 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/ui_data.ts
@@ -13,17 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Layer} from 'trace/flickerlib/common';
+import {Layer} from 'flickerlib/common';
import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {UiRect} from 'viewers/components/rects/types2d';
export class UiData {
dependencies: TraceType[];
- rects: Rectangle[] = [];
+ rects: UiRect[] = [];
displayIds: number[] = [];
- highlightedItems: string[] = [];
+ highlightedItem: string = '';
+ highlightedProperty: string = '';
pinnedItems: HierarchyTreeNode[] = [];
hierarchyUserOptions: UserOptions = {};
propertiesUserOptions: UserOptions = {};
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
index 6868f55..2b63614 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger.ts
@@ -14,16 +14,20 @@
* limitations under the License.
*/
+import {FunctionUtils} from 'common/function_utils';
+import {TabbedViewSwitchRequest, WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {ViewerEvents} from 'viewers/common/viewer_events';
+import {ViewCaptureUtils} from 'viewers/common/view_capture_utils';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Presenter} from './presenter';
import {UiData} from './ui_data';
class ViewerSurfaceFlinger implements Viewer {
static readonly DEPENDENCIES: TraceType[] = [TraceType.SURFACE_FLINGER];
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
private readonly htmlElement: HTMLElement;
private readonly presenter: Presenter;
@@ -38,7 +42,10 @@
this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem)
);
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) =>
- this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)
+ this.presenter.updateHighlightedItem(`${(event as CustomEvent).detail.id}`)
+ );
+ this.htmlElement.addEventListener(ViewerEvents.HighlightedPropertyChange, (event) =>
+ this.presenter.updateHighlightedProperty(`${(event as CustomEvent).detail.id}`)
);
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) =>
this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)
@@ -55,10 +62,28 @@
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) =>
this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)
);
+ this.htmlElement.addEventListener(ViewerEvents.RectsDblClick, (event) => {
+ if (
+ (event as CustomEvent).detail.clickedRectId.includes(
+ ViewCaptureUtils.NEXUS_LAUNCHER_PACKAGE_NAME
+ )
+ ) {
+ this.switchToNexusLauncherViewer();
+ }
+ });
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ // TODO: Make this generic by package name once TraceType is not explicitly defined
+ async switchToNexusLauncherViewer() {
+ await this.emitAppEvent(new TabbedViewSwitchRequest(TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY));
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
index 5613126..21858bc 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/viewer_surface_flinger_component.ts
@@ -28,14 +28,14 @@
class="rects-view"
title="Layers"
[rects]="inputData?.rects ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [highlightedItem]="inputData?.highlightedItem ?? ''"
[displayIds]="inputData?.displayIds ?? []"></rects-view>
<mat-divider [vertical]="true"></mat-divider>
<hierarchy-view
class="hierarchy-view"
[tree]="inputData?.tree ?? null"
[dependencies]="inputData?.dependencies ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [highlightedItem]="inputData?.highlightedItem ?? ''"
[pinnedItems]="inputData?.pinnedItems ?? []"
[store]="store"
[userOptions]="inputData?.hierarchyUserOptions ?? {}"></hierarchy-view>
@@ -44,7 +44,9 @@
class="properties-view"
[userOptions]="inputData?.propertiesUserOptions ?? {}"
[propertiesTree]="inputData?.propertiesTree ?? {}"
- [selectedFlickerItem]="inputData?.selectedLayer ?? {}"
+ [highlightedProperty]="inputData?.highlightedProperty ?? ''"
+ [selectedItem]="inputData?.selectedLayer ?? {}"
+ [traceType]="${TraceType.SURFACE_FLINGER}"
[displayPropertyGroups]="inputData?.displayPropertyGroups"
[isProtoDump]="true"></properties-view>
</div>
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
index 50a5293..ddbf748 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
@@ -17,11 +17,11 @@
import {ArrayUtils} from 'common/array_utils';
import {assertDefined} from 'common/assert_utils';
import {TimeUtils} from 'common/time_utils';
-import {ObjectFormatter} from 'trace/flickerlib/ObjectFormatter';
+import {ObjectFormatter} from 'flickerlib/ObjectFormatter';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace, TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {PropertiesTreeGenerator} from 'viewers/common/properties_tree_generator';
import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -60,21 +60,21 @@
this.notifyUiDataCallback(this.uiData);
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.initializeIfNeeded();
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.initializeIfNeeded();
+ this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+ this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
+ this.uiData.selectedEntryIndex = undefined;
+ this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
+ this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
+ this.uiData.entries,
+ this.uiData.currentEntryIndex,
+ this.uiData.selectedEntryIndex
+ );
- this.entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
-
- this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
- this.uiData.selectedEntryIndex = undefined;
- this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
- this.uiData.currentPropertiesTree = this.computeCurrentPropertiesTree(
- this.uiData.entries,
- this.uiData.currentEntryIndex,
- this.uiData.selectedEntryIndex
- );
-
- this.notifyUiDataCallback(this.uiData);
+ this.notifyUiDataCallback(this.uiData);
+ });
}
onVSyncIdFilterChanged(vsyncIds: string[]) {
@@ -278,9 +278,15 @@
const formattingOptions = ObjectFormatter.displayDefaults;
ObjectFormatter.displayDefaults = true;
+ const entryProtos = await Promise.all(
+ this.trace.mapEntry(async (entry) => {
+ return await entry.getValue();
+ })
+ );
+
for (let originalIndex = 0; originalIndex < this.trace.lengthEntries; ++originalIndex) {
const entry = this.trace.getEntry(originalIndex);
- const entryProto = (await entry.getValue()) as any;
+ const entryProto = entryProtos[originalIndex] as any;
for (const transactionStateProto of entryProto.transactions) {
for (const layerStateProto of transactionStateProto.layerChanges) {
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
index c04bb91..def174b 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
@@ -15,14 +15,14 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {RealTimestamp, TimestampType} from 'common/time';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {TracesBuilder} from 'test/unit/traces_builder';
import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {Presenter} from './presenter';
import {UiData, UiDataEntryType} from './ui_data';
@@ -52,12 +52,12 @@
expect(outputUiData).toEqual(UiData.EMPTY);
- await presenter.onTracePositionUpdate(TracePosition.fromTimestamp(new RealTimestamp(10n)));
+ await presenter.onAppEvent(TracePositionUpdate.fromTimestamp(new RealTimestamp(10n)));
expect(outputUiData).toEqual(UiData.EMPTY);
});
it('processes trace position update and computes output UI data', async () => {
- await presenter.onTracePositionUpdate(createTracePosition(0));
+ await presenter.onAppEvent(createTracePositionUpdate(0));
expect(assertDefined(outputUiData).allPids).toEqual([
'N/A',
@@ -98,11 +98,11 @@
});
it('processes trace position update and updates current entry and scroll position', async () => {
- await presenter.onTracePositionUpdate(createTracePosition(0));
+ await presenter.onAppEvent(createTracePositionUpdate(0));
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
expect(assertDefined(outputUiData).scrollToIndex).toEqual(0);
- await presenter.onTracePositionUpdate(createTracePosition(10));
+ await presenter.onAppEvent(createTracePositionUpdate(10));
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
expect(assertDefined(outputUiData).scrollToIndex).toEqual(13);
});
@@ -234,7 +234,7 @@
});
it('updates selected entry and properties tree when entry is clicked', async () => {
- await presenter.onTracePositionUpdate(createTracePosition(0));
+ await presenter.onAppEvent(createTracePositionUpdate(0));
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
expect(assertDefined(outputUiData).selectedEntryIndex).toBeUndefined();
expect(assertDefined(outputUiData).scrollToIndex).toEqual(0);
@@ -261,15 +261,15 @@
});
it('computes current entry index', async () => {
- await presenter.onTracePositionUpdate(createTracePosition(0));
+ await presenter.onAppEvent(createTracePositionUpdate(0));
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(0);
- await presenter.onTracePositionUpdate(createTracePosition(10));
+ await presenter.onAppEvent(createTracePositionUpdate(10));
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
});
it('updates current entry index when filters change', async () => {
- await presenter.onTracePositionUpdate(createTracePosition(10));
+ await presenter.onAppEvent(createTracePositionUpdate(10));
presenter.onPidFilterChanged([]);
expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
@@ -304,10 +304,11 @@
outputUiData = data;
});
- await presenter.onTracePositionUpdate(createTracePosition(0)); // trigger initialization
+ await presenter.onAppEvent(createTracePositionUpdate(0)); // trigger initialization
};
- const createTracePosition = (entryIndex: number): TracePosition => {
- return TracePosition.fromTraceEntry(trace.getEntry(entryIndex));
+ const createTracePositionUpdate = (entryIndex: number): TracePositionUpdate => {
+ const entry = trace.getEntry(entryIndex);
+ return TracePositionUpdate.fromTraceEntry(entry);
};
});
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
index f10a75c..250b45a 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {WinscopeEvent} from 'messaging/winscope_event';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
@@ -63,8 +63,12 @@
});
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent() {
+ // do nothing
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
index 0158bd0..cb4e2f3 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
@@ -83,7 +83,7 @@
</mat-form-field>
</div>
<div class="what">
- <mat-form-field appearance="fill">
+ <mat-form-field appearance="fill" (keydown.enter)="$event.target.blur()">
<mat-label>Search text</mat-label>
<input matInput [(ngModel)]="whatSearchString" (input)="onWhatSearchStringChange()" />
</mat-form-field>
diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter.ts b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
index 21e1ad1..929453a 100644
--- a/tools/winscope/src/viewers/viewer_transitions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
@@ -16,19 +16,23 @@
import {assertDefined} from 'common/assert_utils';
import {TimeUtils} from 'common/time_utils';
-import {LayerTraceEntry, Transition, WindowManagerState} from 'trace/flickerlib/common';
+import {LayerTraceEntry, Transition, WindowManagerState} from 'flickerlib/common';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
+import {CustomQueryType} from 'trace/custom_query';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UiData} from './ui_data';
export class Presenter {
+ private isInitialized = false;
private transitionTrace: Trace<object>;
- private surfaceFlingerTrace: Trace<object> | undefined;
- private windowManagerTrace: Trace<object> | undefined;
+ private surfaceFlingerTrace: Trace<LayerTraceEntry> | undefined;
+ private windowManagerTrace: Trace<WindowManagerState> | undefined;
+ private layerIdToName = new Map<number, string>();
+ private windowTokenToTitle = new Map<string, string>();
private uiData = UiData.EMPTY;
private readonly notifyUiDataCallback: (data: UiData) => void;
@@ -39,23 +43,57 @@
this.notifyUiDataCallback = notifyUiDataCallback;
}
- async onTracePositionUpdate(position: TracePosition) {
- if (this.uiData === UiData.EMPTY) {
- this.uiData = await this.computeUiData();
- }
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.initializeIfNeeded();
- const entry = TraceEntryFinder.findCorrespondingEntry(this.transitionTrace, position);
+ if (this.uiData === UiData.EMPTY) {
+ this.uiData = await this.computeUiData();
+ }
- this.uiData.selectedTransition = await entry?.getValue();
+ const entry = TraceEntryFinder.findCorrespondingEntry(this.transitionTrace, event.position);
+ this.uiData.selectedTransition = await entry?.getValue();
+ if (this.uiData.selectedTransition !== undefined) {
+ await this.onTransitionSelected(this.uiData.selectedTransition);
+ }
+
+ this.notifyUiDataCallback(this.uiData);
+ });
+ }
+
+ async onTransitionSelected(transition: Transition): Promise<void> {
+ this.uiData.selectedTransition = transition;
+ this.uiData.selectedTransitionPropertiesTree = await this.makeSelectedTransitionPropertiesTree(
+ transition
+ );
this.notifyUiDataCallback(this.uiData);
}
- onTransitionSelected(transition: Transition): void {
- this.uiData.selectedTransition = transition;
- this.uiData.selectedTransitionPropertiesTree =
- this.makeSelectedTransitionPropertiesTree(transition);
- this.notifyUiDataCallback(this.uiData);
+ private async initializeIfNeeded() {
+ if (this.isInitialized) {
+ return;
+ }
+
+ if (this.surfaceFlingerTrace) {
+ const layersIdAndName = await this.surfaceFlingerTrace.customQuery(
+ CustomQueryType.SF_LAYERS_ID_AND_NAME
+ );
+ layersIdAndName.forEach((value) => {
+ this.layerIdToName.set(value.id, value.name);
+ });
+ }
+
+ if (this.windowManagerTrace) {
+ const windowsTokenAndTitle = await this.windowManagerTrace.customQuery(
+ CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE
+ );
+ windowsTokenAndTitle.forEach((value) => {
+ this.windowTokenToTitle.set(value.token, value.title);
+ });
+ }
+
+ this.isInitialized = true;
}
private async computeUiData(): Promise<UiData> {
@@ -80,44 +118,18 @@
);
}
- private makeSelectedTransitionPropertiesTree(transition: Transition): PropertiesTreeNode {
+ private async makeSelectedTransitionPropertiesTree(
+ transition: Transition
+ ): Promise<PropertiesTreeNode> {
const changes: PropertiesTreeNode[] = [];
for (const change of transition.changes) {
- let layerName: string | undefined = undefined;
- let windowName: string | undefined = undefined;
-
- if (this.surfaceFlingerTrace) {
- this.surfaceFlingerTrace.forEachEntry((entry, originalIndex) => {
- if (layerName !== undefined) {
- return;
- }
- const layerTraceEntry = entry.getValue() as LayerTraceEntry;
- for (const layer of layerTraceEntry.flattenedLayers) {
- if (layer.id === change.layerId) {
- layerName = layer.name;
- }
- }
- });
- }
-
- if (this.windowManagerTrace) {
- this.windowManagerTrace.forEachEntry((entry, originalIndex) => {
- if (windowName !== undefined) {
- return;
- }
- const wmState = entry.getValue() as WindowManagerState;
- for (const window of wmState.windowContainers) {
- if (window.token.toLowerCase() === change.windowId.toString(16).toLowerCase()) {
- windowName = window.title;
- }
- }
- });
- }
+ const layerName = this.layerIdToName.get(change.layerId);
+ const windowTitle = this.windowTokenToTitle.get(change.windowId.toString(16));
const layerIdValue = layerName ? `${change.layerId} (${layerName})` : change.layerId;
- const windowIdValue = windowName
- ? `0x${change.windowId.toString(16)} (${windowName})`
+ const windowIdValue = windowTitle
+ ? `0x${change.windowId.toString(16)} (${windowTitle})`
: `0x${change.windowId.toString(16)}`;
changes.push({
@@ -194,8 +206,11 @@
});
}
- if (transition.mergedInto) {
- properties.push({propertyKey: 'mergedInto', propertyValue: transition.mergedInto});
+ if (transition.mergeTarget) {
+ properties.push({
+ propertyKey: 'mergeTarget',
+ propertyValue: transition.mergeTarget,
+ });
}
if (transition.startTransactionId !== -1) {
diff --git a/tools/winscope/src/viewers/viewer_transitions/ui_data.ts b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
index 8676b99..da0cbc1 100644
--- a/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/ui_data.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import {Transition} from 'trace/flickerlib/common';
-import {TimestampType} from 'trace/timestamp';
+import {TimestampType} from 'common/time';
+import {Transition} from 'flickerlib/common';
import {PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
export class UiData {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
index a66bbc6..91d7bbf 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {WinscopeEvent} from 'messaging/winscope_event';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
@@ -34,8 +34,12 @@
});
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent() {
+ // do nothing
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
index f642223..fdf3b4c 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
@@ -15,9 +15,9 @@
*/
import {Component, ElementRef, Inject, Input} from '@angular/core';
+import {ElapsedTimestamp, TimestampType} from 'common/time';
import {TimeUtils} from 'common/time_utils';
-import {Transition} from 'trace/flickerlib/common';
-import {ElapsedTimestamp, TimestampType} from 'trace/timestamp';
+import {Transition} from 'flickerlib/common';
import {Terminal} from 'viewers/common/ui_tree_utils';
import {Events} from './events';
import {UiData} from './ui_data';
@@ -26,118 +26,88 @@
selector: 'viewer-transitions',
template: `
<div class="card-grid container">
- <div class="top-viewer">
- <div class="entries">
- <div class="table-header table-row">
- <div class="id">Id</div>
- <div class="type">Type</div>
- <div class="send-time">Send Time</div>
- <div class="duration">Duration</div>
- <div class="status">Status</div>
- </div>
- <cdk-virtual-scroll-viewport itemSize="53" class="scroll">
- <div
- *cdkVirtualFor="let transition of uiData.entries; let i = index"
- class="entry table-row"
- [class.current]="isCurrentTransition(transition)"
- (click)="onTransitionClicked(transition)">
- <div class="id">
- <span class="mat-body-1">{{ transition.id }}</span>
+ <div class="entries">
+ <div class="table-header table-row">
+ <div class="id">Id</div>
+ <div class="type">Type</div>
+ <div class="send-time">Send Time</div>
+ <div class="duration">Duration</div>
+ <div class="status">Status</div>
+ </div>
+ <cdk-virtual-scroll-viewport itemSize="53" class="scroll">
+ <div
+ *cdkVirtualFor="let transition of uiData.entries; let i = index"
+ class="entry table-row"
+ [class.current]="isCurrentTransition(transition)"
+ (click)="onTransitionClicked(transition)">
+ <div class="id">
+ <span class="mat-body-1">{{ transition.id }}</span>
+ </div>
+ <div class="type">
+ <span class="mat-body-1">{{ transition.type }}</span>
+ </div>
+ <div class="send-time">
+ <span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{
+ formattedTime(transition.sendTime, uiData.timestampType)
+ }}</span>
+ <span *ngIf="transition.sendTime.isMin"> n/a </span>
+ </div>
+ <div class="duration">
+ <span
+ *ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax"
+ class="mat-body-1"
+ >{{
+ formattedTimeDiff(
+ transition.sendTime,
+ transition.finishTime,
+ uiData.timestampType
+ )
+ }}</span
+ >
+ <span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span>
+ </div>
+ <div class="status">
+ <div *ngIf="transition.merged">
+ <span>MERGED</span>
+ <mat-icon aria-hidden="false" fontIcon="merge" matTooltip="merged" icon-gray>
+ </mat-icon>
</div>
- <div class="type">
- <span class="mat-body-1">{{ transition.type }}</span>
- </div>
- <div class="send-time">
- <span *ngIf="!transition.sendTime.isMin" class="mat-body-1">{{
- formattedTime(transition.sendTime, uiData.timestampType)
- }}</span>
- <span *ngIf="transition.sendTime.isMin"> n/a </span>
- </div>
- <div class="duration">
- <span
- *ngIf="!transition.sendTime.isMin && !transition.finishTime.isMax"
- class="mat-body-1"
- >{{
- formattedTimeDiff(
- transition.sendTime,
- transition.finishTime,
- uiData.timestampType
- )
- }}</span
- >
- <span *ngIf="transition.sendTime.isMin || transition.finishTime.isMax">n/a</span>
- </div>
- <div class="status">
- <div *ngIf="transition.mergedInto">
- <span>MERGED</span>
- <mat-icon aria-hidden="false" fontIcon="merge" matTooltip="merged" icon-gray>
- </mat-icon>
- </div>
- <div *ngIf="transition.aborted && !transition.mergedInto">
- <span>ABORTED</span>
- <mat-icon
- aria-hidden="false"
- fontIcon="close"
- matTooltip="aborted"
- style="color: red"
- icon-red></mat-icon>
- </div>
+ <div *ngIf="transition.aborted && !transition.merged">
+ <span>ABORTED</span>
+ <mat-icon
+ aria-hidden="false"
+ fontIcon="close"
+ matTooltip="aborted"
+ style="color: red"
+ icon-red></mat-icon>
+ </div>
- <div *ngIf="transition.played && !transition.aborted && !transition.mergedInto">
- <span>PLAYED</span>
- <mat-icon
- aria-hidden="false"
- fontIcon="check"
- matTooltip="played"
- style="color: green"
- *ngIf="
- transition.played && !transition.aborted && !transition.mergedInto
- "></mat-icon>
- </div>
+ <div *ngIf="transition.played && !transition.aborted && !transition.merged">
+ <span>PLAYED</span>
+ <mat-icon
+ aria-hidden="false"
+ fontIcon="check"
+ matTooltip="played"
+ style="color: green"
+ *ngIf="transition.played && !transition.aborted && !transition.merged"></mat-icon>
</div>
</div>
- </cdk-virtual-scroll-viewport>
- </div>
-
- <mat-divider [vertical]="true"></mat-divider>
-
- <div class="container-properties">
- <h3 class="properties-title mat-title">Selected Transition</h3>
- <tree-view
- [item]="uiData.selectedTransitionPropertiesTree"
- [showNode]="showNode"
- [isLeaf]="isLeaf"
- [isAlwaysCollapsed]="true">
- </tree-view>
- <div *ngIf="!uiData.selectedTransitionPropertiesTree">
- No selected transition.<br />
- Select the tranitions below.
</div>
- </div>
+ </cdk-virtual-scroll-viewport>
</div>
- <div class="bottom-viewer">
- <div class="transition-timeline">
- <div *ngFor="let row of timelineRows()" class="row">
- <svg width="100%" [attr.height]="transitionHeight">
- <rect
- *ngFor="let transition of transitionsOnRow(row)"
- [attr.width]="widthOf(transition)"
- [attr.height]="transitionHeight"
- [attr.style]="transitionRectStyle(transition)"
- rx="5"
- [attr.x]="startOf(transition)"
- (click)="onTransitionClicked(transition)" />
- <rect
- *ngFor="let transition of transitionsOnRow(row)"
- [attr.width]="transitionDividerWidth"
- [attr.height]="transitionHeight"
- [attr.style]="transitionDividerRectStyle(transition)"
- [attr.x]="sendOf(transition)" />
- </svg>
- </div>
- </div>
+ <mat-divider [vertical]="true"></mat-divider>
+
+ <div class="container-properties">
+ <h3 class="properties-title mat-title">Selected Transition</h3>
+ <tree-view
+ [item]="uiData.selectedTransitionPropertiesTree"
+ [showNode]="showNode"
+ [isLeaf]="isLeaf"
+ [isAlwaysCollapsed]="true">
+ </tree-view>
+ <div *ngIf="!uiData.selectedTransitionPropertiesTree">No selected transition.</div>
</div>
</div>
`,
@@ -146,24 +116,7 @@
.container {
display: flex;
flex-grow: 1;
- flex-direction: column;
- }
-
- .top-viewer {
- display: flex;
- flex-grow: 1;
- flex: 3;
- border-bottom: solid 1px rgba(0, 0, 0, 0.12);
- }
-
- .bottom-viewer {
- display: flex;
- flex-shrink: 1;
- }
-
- .transition-timeline {
- flex-grow: 1;
- padding: 1.5rem 1rem;
+ flex-direction: row;
}
.entries {
@@ -176,6 +129,7 @@
.container-properties {
flex: 1;
padding: 16px;
+ overflow-y: scroll;
}
.entries .scroll {
@@ -450,7 +404,7 @@
return Math.max(...this.assignRowsToTransitions().values());
}
- private emitEvent(event: string, data: any) {
+ emitEvent(event: string, data: any) {
const customEvent = new CustomEvent(event, {
bubbles: true,
detail: data,
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
index feb61c5..1f1746a 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
@@ -18,6 +18,8 @@
import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
import {MatDividerModule} from '@angular/material/divider';
+import {assertDefined} from 'common/assert_utils';
+import {TimestampType} from 'common/time';
import {
CrossPlatform,
ShellTransitionData,
@@ -25,8 +27,19 @@
TransitionChange,
TransitionType,
WmTransitionData,
-} from 'trace/flickerlib/common';
-import {TimestampType} from 'trace/timestamp';
+} from 'flickerlib/common';
+import {TracePositionUpdate} from 'messaging/winscope_event';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TracePosition} from 'trace/trace_position';
+import {TraceType} from 'trace/trace_type';
+import {TreeComponent} from 'viewers/components/tree_component';
+import {TreeNodeComponent} from 'viewers/components/tree_node_component';
+import {TreeNodeDataViewComponent} from 'viewers/components/tree_node_data_view_component';
+import {TreeNodePropertiesDataViewComponent} from 'viewers/components/tree_node_properties_data_view_component';
+import {Events} from './events';
+import {Presenter} from './presenter';
import {UiData} from './ui_data';
import {ViewerTransitionsComponent} from './viewer_transitions_component';
@@ -35,11 +48,17 @@
let component: ViewerTransitionsComponent;
let htmlElement: HTMLElement;
- beforeAll(async () => {
+ beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
imports: [MatDividerModule, ScrollingModule],
- declarations: [ViewerTransitionsComponent],
+ declarations: [
+ ViewerTransitionsComponent,
+ TreeComponent,
+ TreeNodeComponent,
+ TreeNodeDataViewComponent,
+ TreeNodePropertiesDataViewComponent,
+ ],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
}).compileComponents();
@@ -69,14 +88,85 @@
'No selected transition'
);
});
+
+ it('emits TransitionSelected event on transition clicked', () => {
+ const emitEventSpy = spyOn(component, 'emitEvent');
+
+ const entries = htmlElement.querySelectorAll('.entry.table-row');
+ const entry1 = assertDefined(entries[0]) as HTMLElement;
+ const entry2 = assertDefined(entries[1]) as HTMLElement;
+ const treeView = assertDefined(
+ htmlElement.querySelector('.container-properties')
+ ) as HTMLElement;
+ expect(treeView.textContent).toContain('No selected transition');
+
+ expect(emitEventSpy).not.toHaveBeenCalled();
+
+ const id0 = assertDefined(entry1.querySelector('.id')).textContent;
+ expect(id0).toBe('0');
+ entry1.click();
+ fixture.detectChanges();
+
+ expect(emitEventSpy).toHaveBeenCalled();
+ expect(emitEventSpy).toHaveBeenCalledWith(Events.TransitionSelected, jasmine.any(Object));
+ expect(emitEventSpy.calls.mostRecent().args[1].id).toBe(0);
+
+ const id1 = assertDefined(entry2.querySelector('.id')).textContent;
+ expect(id1).toBe('1');
+ entry2.click();
+ fixture.detectChanges();
+
+ expect(emitEventSpy).toHaveBeenCalled();
+ expect(emitEventSpy).toHaveBeenCalledWith(Events.TransitionSelected, jasmine.any(Object));
+ expect(emitEventSpy.calls.mostRecent().args[1].id).toBe(1);
+ });
+
+ it('updates tree view on TracePositionUpdate event', async () => {
+ const parser = await UnitTestUtils.getTracesParser([
+ 'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
+ 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
+ ]);
+ const trace = Trace.fromParser(parser, TimestampType.REAL);
+ const traces = new Traces();
+ traces.setTrace(TraceType.TRANSITION, trace);
+
+ let treeView = assertDefined(
+ htmlElement.querySelector('.container-properties')
+ ) as any as HTMLElement;
+ expect(treeView.textContent).toContain('No selected transition');
+
+ const presenter = new Presenter(traces, (data) => {
+ component.inputData = data;
+ });
+ const selectedTransitionEntry = assertDefined(
+ traces.getTrace(TraceType.TRANSITION)?.getEntry(2)
+ );
+ const selectedTransition = (await selectedTransitionEntry.getValue()) as Transition;
+ await presenter.onAppEvent(
+ new TracePositionUpdate(TracePosition.fromTraceEntry(selectedTransitionEntry))
+ );
+
+ expect(component.uiData.selectedTransition.id).toBe(selectedTransition.id);
+ expect(component.uiData.selectedTransitionPropertiesTree).toBeTruthy();
+
+ fixture.detectChanges();
+
+ treeView = assertDefined(
+ fixture.nativeElement.querySelector('.container-properties')
+ ) as any as HTMLElement;
+ const textContentWithoutWhitespaces = treeView.textContent?.replace(/(\s|\t|\n)*/g, '');
+ expect(textContentWithoutWhitespaces).toContain(`id:${selectedTransition.id}`);
+ });
});
function makeUiData(): UiData {
+ let mockTransitionIdCounter = 0;
+
const transitions = [
- createMockTransition(10, 20, 30),
- createMockTransition(40, 42, 50),
- createMockTransition(45, 46, 49),
- createMockTransition(55, 58, 70),
+ createMockTransition(10, 20, 30, mockTransitionIdCounter++),
+ createMockTransition(40, 42, 50, mockTransitionIdCounter++),
+ createMockTransition(45, 46, 49, mockTransitionIdCounter++),
+ createMockTransition(55, 58, 70, mockTransitionIdCounter++),
];
const selectedTransition = undefined;
@@ -94,12 +184,14 @@
function createMockTransition(
createTimeNanos: number,
sendTimeNanos: number,
- finishTimeNanos: number
+ finishTimeNanos: number,
+ id: number
): Transition {
const createTime = CrossPlatform.timestamp.fromString(createTimeNanos.toString(), null, null);
const sendTime = CrossPlatform.timestamp.fromString(sendTimeNanos.toString(), null, null);
const abortTime = null;
const finishTime = CrossPlatform.timestamp.fromString(finishTimeNanos.toString(), null, null);
+ const startingWindowRemoveTime = null;
const startTransactionId = '-1';
const finishTransactionId = '-1';
@@ -107,12 +199,13 @@
const changes: TransitionChange[] = [];
return new Transition(
- id++,
+ id,
new WmTransitionData(
createTime,
sendTime,
abortTime,
finishTime,
+ startingWindowRemoveTime,
startTransactionId,
finishTransactionId,
type,
@@ -121,5 +214,3 @@
new ShellTransitionData()
);
}
-
-let id = 0;
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
index a8ddf70..97e446b 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter.ts
@@ -17,24 +17,28 @@
import {assertDefined} from 'common/assert_utils';
import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {FilterType, TreeUtils} from 'common/tree_utils';
-import {Point} from 'trace/flickerlib/common';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
-import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
+import {FrameData, TraceType, ViewNode} from 'trace/trace_type';
+import {SurfaceFlingerUtils} from 'viewers/common/surface_flinger_utils';
import {TreeGenerator} from 'viewers/common/tree_generator';
import {TreeTransformer} from 'viewers/common/tree_transformer';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {ViewCaptureUtils} from 'viewers/common/view_capture_utils';
+import {UiRect} from 'viewers/components/rects/types2d';
import {UiData} from './ui_data';
export class Presenter {
- private viewCaptureTrace: Trace<object>;
+ private readonly traces: Traces;
+ private readonly surfaceFlingerTrace: Trace<object> | undefined;
+ private readonly viewCaptureTrace: Trace<object>;
+ private viewCapturePackageNames: string[] = [];
- private selectedFrameData: any | undefined;
- private previousFrameData: any | undefined;
+ private selectedFrameData: FrameData | undefined;
+ private previousFrameData: FrameData | undefined;
private selectedHierarchyTree: HierarchyTreeNode | undefined;
private uiData: UiData | undefined;
@@ -42,7 +46,7 @@
private pinnedItems: HierarchyTreeNode[] = [];
private pinnedIds: string[] = [];
- private highlightedItems: string[] = [];
+ private highlightedItem: string = '';
private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
@@ -87,25 +91,51 @@
);
constructor(
+ traceType: TraceType,
traces: Traces,
private readonly storage: Storage,
private readonly notifyUiDataCallback: (data: UiData) => void
) {
- this.viewCaptureTrace = assertDefined(traces.getTrace(TraceType.VIEW_CAPTURE));
+ this.traces = traces;
+ this.viewCaptureTrace = assertDefined(traces.getTrace(traceType));
+ this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
}
- async onTracePositionUpdate(position: TracePosition) {
- const entry = TraceEntryFinder.findCorrespondingEntry(this.viewCaptureTrace, position);
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ await this.initializeIfNeeded();
- let prevEntry: typeof entry;
- if (entry && entry.getIndex() > 0) {
- prevEntry = this.viewCaptureTrace.getEntry(entry.getIndex() - 1);
- }
+ const vcEntry = TraceEntryFinder.findCorrespondingEntry(
+ this.viewCaptureTrace,
+ event.position
+ );
+ let prevVcEntry: typeof vcEntry;
+ if (vcEntry && vcEntry.getIndex() > 0) {
+ prevVcEntry = this.viewCaptureTrace.getEntry(vcEntry.getIndex() - 1);
+ }
- this.selectedFrameData = await entry?.getValue();
- this.previousFrameData = await prevEntry?.getValue();
+ this.selectedFrameData = await vcEntry?.getValue();
+ this.previousFrameData = await prevVcEntry?.getValue();
- this.refreshUI();
+ if (this.uiData && this.surfaceFlingerTrace) {
+ const surfaceFlingerEntry = await TraceEntryFinder.findCorrespondingEntry(
+ this.surfaceFlingerTrace,
+ event.position
+ )?.getValue();
+ if (surfaceFlingerEntry) {
+ this.uiData.sfRects = SurfaceFlingerUtils.makeRects(
+ surfaceFlingerEntry,
+ this.viewCapturePackageNames,
+ this.hierarchyUserOptions
+ );
+ }
+ }
+ this.refreshUI();
+ });
+ }
+
+ private async initializeIfNeeded() {
+ this.viewCapturePackageNames = await ViewCaptureUtils.getPackageNames(this.traces);
}
private refreshUI() {
@@ -114,42 +144,51 @@
if (!this.selectedHierarchyTree && tree) {
this.selectedHierarchyTree = tree;
}
+ let selectedViewNode: any | null = null;
+ if (this.selectedHierarchyTree?.name && this.selectedFrameData?.node) {
+ selectedViewNode = this.findViewNode(
+ this.selectedHierarchyTree.name,
+ this.selectedFrameData.node
+ );
+ }
this.uiData = new UiData(
- this.generateRectangles(),
+ this.generateViewCaptureUiRects(),
+ this.uiData?.sfRects,
tree,
this.hierarchyUserOptions,
this.propertiesUserOptions,
this.pinnedItems,
- this.highlightedItems,
- this.getTreeWithTransformedProperties(this.selectedHierarchyTree)
+ this.highlightedItem,
+ this.getTreeWithTransformedProperties(this.selectedHierarchyTree),
+ selectedViewNode
);
+
this.notifyUiDataCallback(this.uiData);
}
- private generateRectangles(): Rectangle[] {
- const rectangles: Rectangle[] = [];
+ private generateViewCaptureUiRects(): UiRect[] {
+ const rectangles: UiRect[] = [];
function inner(node: any /* ViewNode */) {
- const aRectangle: Rectangle = {
- topLeft: new Point(node.boxPos.left, node.boxPos.top),
- bottomRight: new Point(
- node.boxPos.left + node.boxPos.width,
- node.boxPos.top + node.boxPos.height
- ),
+ const aUiRect: UiRect = {
+ x: node.boxPos.left,
+ y: node.boxPos.top,
+ w: node.boxPos.width,
+ h: node.boxPos.height,
label: '',
- transform: null,
+ transform: undefined,
isVisible: node.isVisible,
isDisplay: false,
- ref: {},
id: node.id,
displayId: 0,
isVirtual: false,
isClickable: true,
cornerRadius: 0,
depth: node.depth,
+ hasContent: node.isVisible,
};
- rectangles.push(aRectangle);
+ rectangles.push(aUiRect);
node.children.forEach((it: any) /* ViewNode */ => inner(it));
}
if (this.selectedFrameData?.node) {
@@ -204,18 +243,17 @@
}
}
- updateHighlightedItems(id: string) {
- if (this.highlightedItems.includes(id)) {
- this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
+ updateHighlightedItem(id: string) {
+ if (this.highlightedItem === id) {
+ this.highlightedItem = '';
} else {
- this.highlightedItems = [];
- this.highlightedItems.push(id);
+ this.highlightedItem = id;
}
- this.uiData!!.highlightedItems = this.highlightedItems;
+ this.uiData!!.highlightedItem = this.highlightedItem;
this.copyUiDataAndNotifyView();
}
- updateHierarchyTree(userOptions: any) {
+ updateHierarchyTree(userOptions: UserOptions) {
this.hierarchyUserOptions = userOptions;
this.uiData!!.hierarchyUserOptions = this.hierarchyUserOptions;
this.uiData!!.tree = this.generateTree();
@@ -241,6 +279,10 @@
newPropertiesTree(selectedItem: HierarchyTreeNode) {
this.selectedHierarchyTree = selectedItem;
+ this.uiData!!.selectedViewNode = this.findViewNode(
+ selectedItem.name,
+ this.selectedFrameData.node
+ );
this.updateSelectedTreeUiData();
}
@@ -251,7 +293,23 @@
this.copyUiDataAndNotifyView();
}
- private getTreeWithTransformedProperties(selectedTree: any): PropertiesTreeNode | null {
+ private findViewNode(name: string, root: ViewNode): ViewNode | null {
+ if (name === root.name) {
+ return root;
+ } else {
+ for (let i = 0; i < root.children.length; i++) {
+ const subTreeSearch = this.findViewNode(name, root.children[i]);
+ if (subTreeSearch != null) {
+ return subTreeSearch;
+ }
+ }
+ }
+ return null;
+ }
+
+ private getTreeWithTransformedProperties(
+ selectedTree: HierarchyTreeNode | undefined
+ ): PropertiesTreeNode | null {
if (!selectedTree) {
return null;
}
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
index 0a7e14a..e8a9932 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
@@ -14,16 +14,16 @@
* limitations under the License.
*/
+import {RealTimestamp} from 'common/time';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
import {MockStorage} from 'test/unit/mock_storage';
import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {Point} from 'trace/flickerlib/common';
+import {CustomQueryType} from 'trace/custom_query';
import {Parser} from 'trace/parser';
-import {RealTimestamp, TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {HierarchyTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
@@ -35,7 +35,7 @@
let trace: Trace<object>;
let uiData: UiData;
let presenter: Presenter;
- let position: TracePosition;
+ let positionUpdate: TracePositionUpdate;
let selectedTree: HierarchyTreeNode;
beforeAll(async () => {
@@ -43,13 +43,10 @@
'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc'
);
trace = new TraceBuilder<object>()
- .setEntries([
- parser.getEntry(0, TimestampType.REAL),
- parser.getEntry(1, TimestampType.REAL),
- parser.getEntry(2, TimestampType.REAL),
- ])
+ .setType(TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY)
+ .setParser(parser)
.build();
- position = TracePosition.fromTraceEntry(trace.getEntry(0));
+ positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
selectedTree = new HierarchyTreeBuilder()
.setName('Name@Id')
.setStableId('stableId')
@@ -59,25 +56,31 @@
.build();
});
- beforeEach(async () => {
+ beforeEach(() => {
presenter = createPresenter(trace);
});
it('is robust to empty trace', async () => {
- const emptyTrace = new TraceBuilder<object>().setEntries([]).build();
+ const emptyTrace = new TraceBuilder<object>()
+ .setType(TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY)
+ .setEntries([])
+ .setParserCustomQueryResult(CustomQueryType.VIEW_CAPTURE_PACKAGE_NAME, 'the_package_name')
+ .build();
const presenter = createPresenter(emptyTrace);
- const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
- await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+ const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+ new RealTimestamp(0n)
+ );
+ await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
});
it('processes trace position updates', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.rects.length).toBeGreaterThan(0);
- expect(uiData.highlightedItems?.length).toEqual(0);
+ expect(uiData.highlightedItem?.length).toEqual(0);
const hierarchyOpts = Object.keys(uiData.hierarchyUserOptions);
expect(hierarchyOpts).toBeTruthy();
const propertyOpts = Object.keys(uiData.propertiesUserOptions);
@@ -86,40 +89,42 @@
});
it('creates input data for rects view', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.rects.length).toBeGreaterThan(0);
- expect(uiData.rects[0].topLeft).toEqual(new Point(0, 0));
- expect(uiData.rects[0].bottomRight).toEqual(new Point(1080, 2340));
+ expect(uiData.rects[0].x).toEqual(0);
+ expect(uiData.rects[0].y).toEqual(0);
+ expect(uiData.rects[0].w).toEqual(1080);
+ expect(uiData.rects[0].h).toEqual(249);
});
it('updates pinned items', async () => {
const pinnedItem = new HierarchyTreeBuilder().setName('FirstPinnedItem').setId('id').build();
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.updatePinnedItems(pinnedItem);
expect(uiData.pinnedItems).toContain(pinnedItem);
});
- it('updates highlighted items', async () => {
- expect(uiData.highlightedItems).toEqual([]);
+ it('updates highlighted item', async () => {
+ await presenter.onAppEvent(positionUpdate);
+ expect(uiData.highlightedItem).toEqual('');
const id = '4';
- await presenter.onTracePositionUpdate(position);
- presenter.updateHighlightedItems(id);
- expect(uiData.highlightedItems).toContain(id);
+ presenter.updateHighlightedItem(id);
+ expect(uiData.highlightedItem).toBe(id);
});
it('updates hierarchy tree', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(
- // DecorView -> LinearLayout -> FrameLayout -> LauncherRootView -> DragLayer -> Workspace
- uiData.tree?.children[0].children[1].children[0].children[0].children[1].id
- ).toEqual('com.android.launcher3.Workspace@251960479');
+ // TaskbarDragLayer -> TaskbarView
+ uiData.tree?.children[0].id
+ ).toEqual('com.android.launcher3.taskbar.TaskbarView@80213537');
const userOptions: UserOptions = {
showDiff: {
name: 'Show diff',
- enabled: true,
+ enabled: false,
},
simplifyNames: {
name: 'Simplify names',
@@ -133,9 +138,9 @@
presenter.updateHierarchyTree(userOptions);
expect(uiData.hierarchyUserOptions).toEqual(userOptions);
expect(
- // DecorView -> LinearLayout -> FrameLayout (before, this was the 2nd child) -> LauncherRootView -> DragLayer -> Workspace if filter works as expected
- uiData.tree?.children[0].children[0].children[0].children[0].children[1].id
- ).toEqual('com.android.launcher3.Workspace@251960479');
+ // TaskbarDragLayer -> TaskbarScrimView
+ uiData.tree?.children[0].id
+ ).toEqual('com.android.launcher3.taskbar.TaskbarScrimView@114418695');
});
it('filters hierarchy tree', async () => {
@@ -152,23 +157,19 @@
name: 'Only visible',
enabled: false,
},
- flat: {
- name: 'Flat',
- enabled: true,
- },
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.updateHierarchyTree(userOptions);
- presenter.filterHierarchyTree('Workspace');
+ presenter.filterHierarchyTree('BubbleBarView');
expect(
- // DecorView -> LinearLayout -> FrameLayout -> LauncherRootView -> DragLayer -> Workspace if filter works as expected
- uiData.tree?.children[0].children[0].children[0].children[0].children[0].id
- ).toEqual('com.android.launcher3.Workspace@251960479');
+ // TaskbarDragLayer -> BubbleBarView if filter works as expected
+ uiData.tree?.children[0].id
+ ).toEqual('com.android.launcher3.taskbar.bubbles.BubbleBarView@256010548');
});
it('sets properties tree and associated ui data', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
expect(uiData.propertiesTree).toBeTruthy();
});
@@ -190,7 +191,7 @@
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
expect(uiData.propertiesTree?.diffType).toBeFalsy();
@@ -200,7 +201,7 @@
});
it('filters properties tree', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
const userOptions: UserOptions = {
showDiff: {
@@ -221,7 +222,7 @@
let nonTerminalChildren = uiData.propertiesTree?.children?.filter(
(it) => typeof it.propertyKey === 'string'
);
- expect(nonTerminalChildren?.length).toEqual(24);
+ expect(nonTerminalChildren?.length).toEqual(25);
presenter.filterPropertiesTree('alpha');
nonTerminalChildren = uiData.propertiesTree?.children?.filter(
@@ -232,8 +233,8 @@
const createPresenter = (trace: Trace<object>): Presenter => {
const traces = new Traces();
- traces.setTrace(TraceType.VIEW_CAPTURE, trace);
- return new Presenter(traces, new MockStorage(), (newData: UiData) => {
+ traces.setTrace(parser.getTraceType(), trace);
+ return new Presenter(parser.getTraceType(), traces, new MockStorage(), (newData: UiData) => {
uiData = newData;
});
};
diff --git a/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts b/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
index 8d1a138..2b39050 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/ui_data.ts
@@ -14,22 +14,24 @@
* limitations under the License.
*/
-import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
+import {TraceType, ViewNode} from 'trace/trace_type';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {UiRect} from 'viewers/components/rects/types2d';
export class UiData {
readonly dependencies: TraceType[] = [TraceType.VIEW_CAPTURE];
readonly displayPropertyGroups = false;
constructor(
- readonly rects: Rectangle[],
+ readonly rects: UiRect[],
+ public sfRects: UiRect[] | undefined,
public tree: HierarchyTreeNode | null,
public hierarchyUserOptions: UserOptions,
public propertiesUserOptions: UserOptions,
public pinnedItems: HierarchyTreeNode[],
- public highlightedItems: string[],
- public propertiesTree: PropertiesTreeNode | null
+ public highlightedItem: string,
+ public propertiesTree: PropertiesTreeNode | null,
+ public selectedViewNode: ViewNode
) {}
}
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
index 2a8671a..33b6aea 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture.ts
@@ -14,24 +14,25 @@
* limitations under the License.
*/
+import {FunctionUtils} from 'common/function_utils';
+import {TabbedViewSwitchRequest, WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {ViewerEvents} from 'viewers/common/viewer_events';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Presenter} from './presenter';
import {UiData} from './ui_data';
-// TODO: Fix "flatten tree hierarchy view" behavior.
export class ViewerViewCapture implements Viewer {
static readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE];
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
private htmlElement: HTMLElement;
private presenter: Presenter;
constructor(traces: Traces, storage: Storage) {
this.htmlElement = document.createElement('viewer-view-capture');
-
- this.presenter = new Presenter(traces, storage, (data: UiData) => {
+ this.presenter = new Presenter(this.getDependencies()[0], traces, storage, (data: UiData) => {
(this.htmlElement as any).inputData = data;
});
@@ -39,7 +40,7 @@
this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem)
);
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) =>
- this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)
+ this.presenter.updateHighlightedItem(`${(event as CustomEvent).detail.id}`)
);
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) =>
this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)
@@ -56,10 +57,21 @@
this.htmlElement.addEventListener(ViewerEvents.SelectedTreeChange, (event) =>
this.presenter.newPropertiesTree((event as CustomEvent).detail.selectedItem)
);
+ this.htmlElement.addEventListener(ViewerEvents.MiniRectsDblClick, (event) => {
+ this.switchToSurfaceFlingerView();
+ });
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ async switchToSurfaceFlingerView() {
+ await this.emitAppEvent(new TabbedViewSwitchRequest(TraceType.SURFACE_FLINGER));
}
getViews(): View[] {
@@ -68,8 +80,8 @@
ViewType.TAB,
this.getDependencies(),
this.htmlElement,
- 'View Capture',
- TraceType.VIEW_CAPTURE
+ this.getTitle(),
+ this.getDependencies()[0]
),
];
}
@@ -77,4 +89,38 @@
getDependencies(): TraceType[] {
return ViewerViewCapture.DEPENDENCIES;
}
+
+ private getTitle(): string {
+ switch (this.getDependencies()[0]) {
+ case TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER:
+ return 'View Capture - Taskbar';
+ case TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER:
+ return 'View Capture - Taskbar Overlay';
+ default:
+ return 'View Capture - Nexuslauncher';
+ }
+ }
+}
+
+export class ViewerViewCaptureLauncherActivity extends ViewerViewCapture {
+ static override readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE_LAUNCHER_ACTIVITY];
+ override getDependencies(): TraceType[] {
+ return ViewerViewCaptureLauncherActivity.DEPENDENCIES;
+ }
+}
+
+export class ViewerViewCaptureTaskbarDragLayer extends ViewerViewCapture {
+ static override readonly DEPENDENCIES: TraceType[] = [TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER];
+ override getDependencies(): TraceType[] {
+ return ViewerViewCaptureTaskbarDragLayer.DEPENDENCIES;
+ }
+}
+
+export class ViewerViewCaptureTaskbarOverlayDragLayer extends ViewerViewCapture {
+ static override readonly DEPENDENCIES: TraceType[] = [
+ TraceType.VIEW_CAPTURE_TASKBAR_OVERLAY_DRAG_LAYER,
+ ];
+ override getDependencies(): TraceType[] {
+ return ViewerViewCaptureTaskbarOverlayDragLayer.DEPENDENCIES;
+ }
}
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
index 174bb60..9e4935c 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component.ts
@@ -16,6 +16,7 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {PersistentStore} from 'common/persistent_store';
+import {TraceType} from 'trace/trace_type';
import {UiData} from './ui_data';
/**
@@ -31,14 +32,16 @@
title="View Hierarchy Sketch"
[enableShowVirtualButton]="false"
[rects]="inputData?.rects ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [zoomFactor]="4"
+ [miniRects]="inputData?.sfRects ?? []"
+ [highlightedItem]="inputData?.highlightedItem ?? ''"
[displayIds]="[0]"></rects-view>
<mat-divider [vertical]="true"></mat-divider>
<hierarchy-view
class="hierarchy-view"
[tree]="inputData?.tree ?? null"
[dependencies]="inputData?.dependencies ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [highlightedItem]="inputData?.highlightedItem ?? ''"
[pinnedItems]="inputData?.pinnedItems ?? []"
[store]="store"
[userOptions]="inputData?.hierarchyUserOptions ?? {}"></hierarchy-view>
@@ -47,8 +50,9 @@
class="properties-view"
[userOptions]="inputData?.propertiesUserOptions ?? {}"
[propertiesTree]="inputData?.propertiesTree ?? {}"
- [displayPropertyGroups]="inputData?.displayPropertyGroups"
- [isProtoDump]="true">
+ [selectedItem]="inputData?.selectedViewNode ?? null"
+ [traceType]="${TraceType.VIEW_CAPTURE}"
+ [isProtoDump]="false">
</properties-view>
</div>
`,
diff --git a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
index 170acc8..bd267bc 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/viewer_view_capture_component_test.ts
@@ -22,7 +22,7 @@
import {RectsComponent} from 'viewers/components/rects/rects_component';
import {ViewerViewCaptureComponent} from './viewer_view_capture_component';
-describe('ViewerSurfaceFlingerComponent', () => {
+describe('ViewerViewCaptureComponent', () => {
let fixture: ComponentFixture<ViewerViewCaptureComponent>;
let component: ViewerViewCaptureComponent;
let htmlElement: HTMLElement;
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index 6d43714..f5875a8 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -15,21 +15,22 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {TransformMatrix} from 'common/geometry_utils';
import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {FilterType, TreeUtils} from 'common/tree_utils';
-import {DisplayContent} from 'trace/flickerlib/windows/DisplayContent';
-import {WindowManagerState} from 'trace/flickerlib/windows/WindowManagerState';
+import {DisplayContent} from 'flickerlib/windows/DisplayContent';
+import {WindowManagerState} from 'flickerlib/windows/WindowManagerState';
+import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
-import {TracePosition} from 'trace/trace_position';
import {TraceTreeNode} from 'trace/trace_tree_node';
import {TraceType} from 'trace/trace_type';
-import {Rectangle, RectMatrix, RectTransform} from 'viewers/common/rectangle';
import {TreeGenerator} from 'viewers/common/tree_generator';
import {TreeTransformer} from 'viewers/common/tree_transformer';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {UiRect} from 'viewers/components/rects/types2d';
import {UiData} from './ui_data';
type NotifyViewCallbackType = (uiData: UiData) => void;
@@ -40,8 +41,8 @@
private uiData: UiData;
private hierarchyFilter: FilterType = TreeUtils.makeNodeFilter('');
private propertiesFilter: FilterType = TreeUtils.makeNodeFilter('');
- private highlightedItems: string[] = [];
- private displayIds: number[] = [];
+ private highlightedItem: string = '';
+ private highlightedProperty: string = '';
private pinnedItems: HierarchyTreeNode[] = [];
private pinnedIds: string[] = [];
private selectedHierarchyTree: HierarchyTreeNode | null = null;
@@ -53,6 +54,7 @@
showDiff: {
name: 'Show diff',
enabled: false,
+ isUnavailable: false,
},
simplifyNames: {
name: 'Simplify names',
@@ -75,6 +77,7 @@
showDiff: {
name: 'Show diff',
enabled: false,
+ isUnavailable: false,
},
showDefaults: {
name: 'Show defaults',
@@ -97,7 +100,7 @@
this.trace = assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER));
this.notifyViewCallback = notifyViewCallback;
this.uiData = new UiData([TraceType.WINDOW_MANAGER]);
- this.notifyViewCallback(this.uiData);
+ this.copyUiDataAndNotifyView();
}
updatePinnedItems(pinnedItem: HierarchyTreeNode) {
@@ -109,31 +112,40 @@
}
this.updatePinnedIds(pinnedId);
this.uiData.pinnedItems = this.pinnedItems;
- this.notifyViewCallback(this.uiData);
+ this.copyUiDataAndNotifyView();
}
- updateHighlightedItems(id: string) {
- if (this.highlightedItems.includes(id)) {
- this.highlightedItems = this.highlightedItems.filter((hl) => hl !== id);
+ updateHighlightedItem(id: string) {
+ if (this.highlightedItem === id) {
+ this.highlightedItem = '';
} else {
- this.highlightedItems = []; //if multi-select implemented, remove this line
- this.highlightedItems.push(id);
+ this.highlightedItem = id;
}
- this.uiData.highlightedItems = this.highlightedItems;
- this.notifyViewCallback(this.uiData);
+ this.uiData.highlightedItem = this.highlightedItem;
+ this.copyUiDataAndNotifyView();
+ }
+
+ updateHighlightedProperty(id: string) {
+ if (this.highlightedProperty === id) {
+ this.highlightedProperty = '';
+ } else {
+ this.highlightedProperty = id;
+ }
+ this.uiData.highlightedProperty = this.highlightedProperty;
+ this.copyUiDataAndNotifyView();
}
updateHierarchyTree(userOptions: UserOptions) {
this.hierarchyUserOptions = userOptions;
this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
this.uiData.tree = this.generateTree();
- this.notifyViewCallback(this.uiData);
+ this.copyUiDataAndNotifyView();
}
filterHierarchyTree(filterString: string) {
this.hierarchyFilter = TreeUtils.makeNodeFilter(filterString);
this.uiData.tree = this.generateTree();
- this.notifyViewCallback(this.uiData);
+ this.copyUiDataAndNotifyView();
}
updatePropertiesTree(userOptions: UserOptions) {
@@ -152,55 +164,99 @@
this.updateSelectedTreeUiData();
}
- async onTracePositionUpdate(position: TracePosition) {
- this.uiData = new UiData();
- this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
- this.uiData.propertiesUserOptions = this.propertiesUserOptions;
+ async onAppEvent(event: WinscopeEvent) {
+ await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, event.position);
+ const prevEntry =
+ entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
- const entry = TraceEntryFinder.findCorrespondingEntry(this.trace, position);
- const prevEntry =
- entry && entry.getIndex() > 0 ? this.trace.getEntry(entry.getIndex() - 1) : undefined;
+ this.entry = (await entry?.getValue()) ?? null;
+ this.previousEntry = (await prevEntry?.getValue()) ?? null;
+ if (this.hierarchyUserOptions['showDiff'].isUnavailable !== undefined) {
+ this.hierarchyUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+ }
+ if (this.propertiesUserOptions['showDiff'].isUnavailable !== undefined) {
+ this.propertiesUserOptions['showDiff'].isUnavailable = this.previousEntry == null;
+ }
- this.entry = (await entry?.getValue()) ?? null;
- this.previousEntry = (await prevEntry?.getValue()) ?? null;
- if (this.entry) {
- this.uiData.highlightedItems = this.highlightedItems;
- this.uiData.rects = this.generateRects();
- this.uiData.displayIds = this.displayIds;
- this.uiData.tree = this.generateTree();
- }
+ this.uiData = new UiData();
+ this.uiData.hierarchyUserOptions = this.hierarchyUserOptions;
+ this.uiData.propertiesUserOptions = this.propertiesUserOptions;
- this.notifyViewCallback(this.uiData);
+ if (this.entry) {
+ this.uiData.highlightedItem = this.highlightedItem;
+ this.uiData.highlightedProperty = this.highlightedProperty;
+ this.uiData.rects = this.generateRects(this.entry);
+ this.uiData.displayIds = this.getDisplayIds(this.entry);
+ this.uiData.tree = this.generateTree();
+ }
+
+ this.copyUiDataAndNotifyView();
+ });
}
- private generateRects(): Rectangle[] {
- const displayRects: Rectangle[] =
- this.entry?.displays?.map((display: DisplayContent) => {
- const rect = display.displayRect;
- rect.label = `Display - ${display.title}`;
- rect.stableId = display.stableId;
- rect.displayId = display.id;
- rect.isDisplay = true;
- rect.cornerRadius = 0;
- rect.isVirtual = false;
+ private generateRects(entry: TraceTreeNode): UiRect[] {
+ const identityMatrix: TransformMatrix = {
+ dsdx: 1,
+ dsdy: 0,
+ tx: 0,
+ dtdx: 0,
+ dtdy: 1,
+ ty: 0,
+ };
+ const displayRects: UiRect[] =
+ entry.displays?.map((display: DisplayContent) => {
+ const rect: UiRect = {
+ x: display.displayRect.left,
+ y: display.displayRect.top,
+ w: display.displayRect.right - display.displayRect.left,
+ h: display.displayRect.bottom - display.displayRect.top,
+ label: `Display - ${display.title}`,
+ transform: identityMatrix,
+ isVisible: false, //TODO: check if displayRect.ref.isVisible exists
+ isDisplay: true,
+ id: display.stableId,
+ displayId: display.id,
+ isVirtual: false,
+ isClickable: false,
+ cornerRadius: 0,
+ };
return rect;
}) ?? [];
- this.displayIds = [];
- const rects: Rectangle[] =
- this.entry?.windowStates
+
+ const windowRects: UiRect[] =
+ entry.windowStates
?.sort((a: any, b: any) => b.computedZ - a.computedZ)
.map((it: any) => {
- const rect = it.rect;
- rect.id = it.layerId;
- rect.displayId = it.displayId;
- rect.cornerRadius = 0;
- if (!this.displayIds.includes(it.displayId)) {
- this.displayIds.push(it.displayId);
- }
+ const rect: UiRect = {
+ x: it.rect.left,
+ y: it.rect.top,
+ w: it.rect.right - it.rect.left,
+ h: it.rect.bottom - it.rect.top,
+ label: it.rect.label,
+ transform: identityMatrix,
+ isVisible: it.isVisible,
+ isDisplay: false,
+ id: it.stableId,
+ displayId: it.displayId,
+ isVirtual: false, //TODO: is this correct?
+ isClickable: true,
+ cornerRadius: 0,
+ };
return rect;
}) ?? [];
- this.displayIds.sort();
- return this.rectsToUiData(rects.concat(displayRects));
+
+ return windowRects.concat(displayRects);
+ }
+
+ private getDisplayIds(entry: TraceTreeNode): number[] {
+ const ids = new Set<number>();
+ entry.windowStates?.map((it: any) => {
+ ids.add(it.displayId);
+ });
+ return Array.from(ids.values()).sort((a, b) => {
+ return a - b;
+ });
}
private updateSelectedTreeUiData() {
@@ -209,7 +265,7 @@
this.selectedHierarchyTree
);
}
- this.notifyViewCallback(this.uiData);
+ this.copyUiDataAndNotifyView();
}
private generateTree() {
@@ -223,7 +279,10 @@
.setIsFlatView(this.hierarchyUserOptions['flat']?.enabled)
.withUniqueNodeId();
let tree: HierarchyTreeNode | null;
- if (!this.hierarchyUserOptions['showDiff']?.enabled) {
+ if (
+ !this.hierarchyUserOptions['showDiff']?.enabled ||
+ this.hierarchyUserOptions['showDiff']?.isUnavailable
+ ) {
tree = generator.generateTree();
} else {
tree = generator
@@ -236,40 +295,6 @@
return tree;
}
- private rectsToUiData(rects: any[]): Rectangle[] {
- const uiRects: Rectangle[] = [];
- const identityMatrix: RectMatrix = {
- dsdx: 1,
- dsdy: 0,
- tx: 0,
- dtdx: 0,
- dtdy: 1,
- ty: 0,
- };
- rects.forEach((rect: any) => {
- const transform: RectTransform = {
- matrix: identityMatrix,
- };
-
- const newRect: Rectangle = {
- topLeft: {x: rect.left, y: rect.top},
- bottomRight: {x: rect.right, y: rect.bottom},
- label: rect.label,
- transform,
- isVisible: rect.ref?.isVisible ?? false,
- isDisplay: rect.isDisplay ?? false,
- ref: rect.ref,
- id: rect.stableId ?? rect.ref.stableId,
- displayId: rect.displayId ?? rect.ref.stackId,
- isVirtual: rect.isVirtual ?? false,
- isClickable: !rect.isDisplay,
- cornerRadius: rect.cornerRadius,
- };
- uiRects.push(newRect);
- });
- return uiRects;
- }
-
private updatePinnedIds(newId: string) {
if (this.pinnedIds.includes(newId)) {
this.pinnedIds = this.pinnedIds.filter((pinned) => pinned !== newId);
@@ -285,11 +310,21 @@
const transformer = new TreeTransformer(selectedTree, this.propertiesFilter)
.setOnlyProtoDump(true)
.setIsShowDefaults(this.propertiesUserOptions['showDefaults']?.enabled)
- .setIsShowDiff(this.propertiesUserOptions['showDiff']?.enabled)
+ .setIsShowDiff(
+ this.propertiesUserOptions['showDiff']?.enabled &&
+ !this.propertiesUserOptions['showDiff']?.isUnavailable
+ )
.setTransformerOptions({skip: selectedTree.skip})
.setProperties(this.entry)
.setDiffProperties(this.previousEntry);
const transformedTree = transformer.transform();
return transformedTree;
}
+
+ private copyUiDataAndNotifyView() {
+ // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
+ // won't detect the new input
+ const copy = Object.assign({}, this.uiData);
+ this.notifyViewCallback(copy);
+ }
}
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
index 326e7e6..3bdce14 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
@@ -14,15 +14,15 @@
* limitations under the License.
*/
+import {RealTimestamp} from 'common/time';
+import {WindowManagerState} from 'flickerlib/common';
+import {TracePositionUpdate} from 'messaging/winscope_event';
import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
import {MockStorage} from 'test/unit/mock_storage';
import {TraceBuilder} from 'test/unit/trace_builder';
import {UnitTestUtils} from 'test/unit/utils';
-import {WindowManagerState} from 'trace/flickerlib/common';
-import {RealTimestamp} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {VISIBLE_CHIP} from 'viewers/common/chip';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
@@ -32,7 +32,7 @@
describe('PresenterWindowManager', () => {
let trace: Trace<WindowManagerState>;
- let position: TracePosition;
+ let positionUpdate: TracePositionUpdate;
let presenter: Presenter;
let uiData: UiData;
let selectedTree: HierarchyTreeNode;
@@ -42,7 +42,7 @@
.setEntries([await UnitTestUtils.getWindowManagerState()])
.build();
- position = TracePosition.fromTraceEntry(trace.getEntry(0));
+ positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
selectedTree = new HierarchyTreeBuilder()
.setName('ScreenDecorOverlayBottom')
@@ -67,14 +67,16 @@
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
- const positionWithoutTraceEntry = TracePosition.fromTimestamp(new RealTimestamp(0n));
- await presenter.onTracePositionUpdate(positionWithoutTraceEntry);
+ const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
+ new RealTimestamp(0n)
+ );
+ await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
expect(uiData.hierarchyUserOptions).toBeTruthy();
expect(uiData.tree).toBeFalsy();
});
it('processes trace position update', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
const filteredUiDataRectLabels = uiData.rects
?.filter((rect) => rect.isVisible !== undefined)
.map((rect) => rect.label);
@@ -84,7 +86,7 @@
const propertyOpts = uiData.propertiesUserOptions
? Object.keys(uiData.propertiesUserOptions)
: null;
- expect(uiData.highlightedItems?.length).toEqual(0);
+ expect(uiData.highlightedItem?.length).toEqual(0);
expect(filteredUiDataRectLabels?.length).toEqual(14);
expect(uiData.displayIds).toContain(0);
expect(hierarchyOpts).toBeTruthy();
@@ -94,15 +96,31 @@
expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
});
+ it('disables show diff and generates non-diff tree if no prev entry available', async () => {
+ await presenter.onAppEvent(positionUpdate);
+
+ const hierarchyOpts = uiData.hierarchyUserOptions ?? null;
+ expect(hierarchyOpts).toBeTruthy();
+ expect(hierarchyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+ const propertyOpts = uiData.propertiesUserOptions ?? null;
+ expect(propertyOpts).toBeTruthy();
+ expect(propertyOpts!['showDiff'].isUnavailable).toBeTrue();
+
+ expect(Object.keys(uiData.tree!).length > 0).toBeTrue();
+ });
+
it('creates input data for rects view', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.rects.length).toBeGreaterThan(0);
- expect(uiData.rects[0].topLeft).toEqual({x: 0, y: 2326});
- expect(uiData.rects[0].bottomRight).toEqual({x: 1080, y: 2400});
+ expect(uiData.rects[0].x).toEqual(0);
+ expect(uiData.rects[0].y).toEqual(2326);
+ expect(uiData.rects[0].w).toEqual(1080);
+ expect(uiData.rects[0].h).toEqual(74);
});
it('updates pinned items', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.pinnedItems).toEqual([]);
const pinnedItem = new HierarchyTreeBuilder()
@@ -115,11 +133,18 @@
expect(uiData.pinnedItems).toContain(pinnedItem);
});
- it('updates highlighted items', () => {
- expect(uiData.highlightedItems).toEqual([]);
+ it('updates highlighted item', () => {
+ expect(uiData.highlightedItem).toEqual('');
const id = '4';
- presenter.updateHighlightedItems(id);
- expect(uiData.highlightedItems).toContain(id);
+ presenter.updateHighlightedItem(id);
+ expect(uiData.highlightedItem).toBe(id);
+ });
+
+ it('updates highlighted property', () => {
+ expect(uiData.highlightedProperty).toEqual('');
+ const id = '4';
+ presenter.updateHighlightedProperty(id);
+ expect(uiData.highlightedProperty).toBe(id);
});
it('updates hierarchy tree', async () => {
@@ -143,7 +168,7 @@
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
expect(uiData.tree?.children.length).toEqual(1);
presenter.updateHierarchyTree(userOptions);
expect(uiData.hierarchyUserOptions).toEqual(userOptions);
@@ -170,7 +195,7 @@
enabled: true,
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.updateHierarchyTree(userOptions);
expect(uiData.tree?.children.length).toEqual(72);
presenter.filterHierarchyTree('ScreenDecor');
@@ -179,7 +204,7 @@
});
it('sets properties tree and associated ui data', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
// does not check specific tree values as tree transformation method may change
expect(uiData.propertiesTree).toBeTruthy();
@@ -203,7 +228,7 @@
},
};
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
expect(uiData.propertiesTree?.diffType).toBeFalsy();
presenter.updatePropertiesTree(userOptions);
@@ -213,7 +238,7 @@
});
it('filters properties tree', async () => {
- await presenter.onTracePositionUpdate(position);
+ await presenter.onAppEvent(positionUpdate);
presenter.newPropertiesTree(selectedTree);
let nonTerminalChildren =
diff --git a/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts b/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
index 35bcaf7..320615e 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/ui_data.ts
@@ -14,15 +14,16 @@
* limitations under the License.
*/
import {TraceType} from 'trace/trace_type';
-import {Rectangle} from 'viewers/common/rectangle';
import {HierarchyTreeNode, PropertiesTreeNode} from 'viewers/common/ui_tree_utils';
import {UserOptions} from 'viewers/common/user_options';
+import {UiRect} from 'viewers/components/rects/types2d';
export class UiData {
dependencies: TraceType[];
- rects: Rectangle[] = [];
+ rects: UiRect[] = [];
displayIds: number[] = [];
- highlightedItems: string[] = [];
+ highlightedItem: string = '';
+ highlightedProperty: string = '';
pinnedItems: HierarchyTreeNode[] = [];
hierarchyUserOptions: UserOptions = {};
propertiesUserOptions: UserOptions = {};
diff --git a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
index f44a8b7..3d24c54 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
+import {WinscopeEvent} from 'messaging/winscope_event';
import {Traces} from 'trace/traces';
-import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {ViewerEvents} from 'viewers/common/viewer_events';
import {View, Viewer, ViewType} from 'viewers/viewer';
@@ -32,7 +32,10 @@
this.presenter.updatePinnedItems((event as CustomEvent).detail.pinnedItem)
);
this.htmlElement.addEventListener(ViewerEvents.HighlightedChange, (event) =>
- this.presenter.updateHighlightedItems(`${(event as CustomEvent).detail.id}`)
+ this.presenter.updateHighlightedItem(`${(event as CustomEvent).detail.id}`)
+ );
+ this.htmlElement.addEventListener(ViewerEvents.HighlightedPropertyChange, (event) =>
+ this.presenter.updateHighlightedProperty(`${(event as CustomEvent).detail.id}`)
);
this.htmlElement.addEventListener(ViewerEvents.HierarchyUserOptionsChange, (event) =>
this.presenter.updateHierarchyTree((event as CustomEvent).detail.userOptions)
@@ -51,8 +54,12 @@
);
}
- async onTracePositionUpdate(position: TracePosition) {
- await this.presenter.onTracePositionUpdate(position);
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent() {
+ // do nothing
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts
index d588083..907f6aa 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/viewer_window_manager_component.ts
@@ -28,13 +28,13 @@
title="Windows"
[rects]="inputData?.rects ?? []"
[displayIds]="inputData?.displayIds ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"></rects-view>
+ [highlightedItem]="inputData?.highlightedItem ?? ''"></rects-view>
<mat-divider [vertical]="true"></mat-divider>
<hierarchy-view
class="hierarchy-view"
[tree]="inputData?.tree ?? null"
[dependencies]="inputData?.dependencies ?? []"
- [highlightedItems]="inputData?.highlightedItems ?? []"
+ [highlightedItem]="inputData?.highlightedItem ?? ''"
[pinnedItems]="inputData?.pinnedItems ?? []"
[store]="store"
[userOptions]="inputData?.hierarchyUserOptions ?? {}"></hierarchy-view>
@@ -43,6 +43,7 @@
class="properties-view"
[userOptions]="inputData?.propertiesUserOptions ?? {}"
[propertiesTree]="inputData?.propertiesTree ?? {}"
+ [highlightedProperty]="inputData?.highlightedProperty ?? ''"
[isProtoDump]="true"></properties-view>
</div>
`,
diff --git a/tools/winscope/webpack.config.common.js b/tools/winscope/webpack.config.common.js
index 8a3a00c..9580f48 100644
--- a/tools/winscope/webpack.config.common.js
+++ b/tools/winscope/webpack.config.common.js
@@ -19,7 +19,7 @@
module.exports = {
resolve: {
extensions: ['.ts', '.js', '.css'],
- modules: ['node_modules', 'src', 'kotlin_build', path.resolve(__dirname, '../../..')],
+ modules: ['node_modules', 'src', 'deps_build', __dirname, path.resolve(__dirname, '../../..')],
},
resolveLoader: {
@@ -49,7 +49,9 @@
loader: 'proto-loader',
options: {
paths: [
+ __dirname,
path.resolve(__dirname, '../../..'),
+ path.resolve(__dirname, '../../../external/perfetto'),
path.resolve(__dirname, '../../../external/protobuf/src'),
],
},
diff --git a/tools/winscope/webpack.config.dev.js b/tools/winscope/webpack.config.dev.js
index 5857a8e..9db5011 100644
--- a/tools/winscope/webpack.config.dev.js
+++ b/tools/winscope/webpack.config.dev.js
@@ -16,6 +16,7 @@
const {merge} = require('webpack-merge');
const configCommon = require('./webpack.config.common');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+const CopyPlugin = require('copy-webpack-plugin');
const configDev = {
mode: 'development',
@@ -25,12 +26,32 @@
app: './src/main_dev.ts',
},
devtool: 'source-map',
+
+ externals: {
+ fs: 'fs',
+ path: 'path',
+ crypto: 'crypto',
+ },
+
+ node: {
+ global: false,
+ __filename: false,
+ __dirname: false,
+ },
+
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
inject: 'body',
inlineSource: '.(css|js)$',
}),
+ new CopyPlugin({
+ patterns: [
+ 'deps_build/trace_processor/to_be_served/trace_processor.wasm',
+ 'deps_build/trace_processor/to_be_served/engine_bundle.js',
+ {from: 'src/adb/winscope_proxy.py', to: 'winscope_proxy.py'},
+ ],
+ }),
],
};
diff --git a/tools/winscope/webpack.config.unit_test.js b/tools/winscope/webpack.config.unit_test.js
deleted file mode 100644
index 15b4ed4..0000000
--- a/tools/winscope/webpack.config.unit_test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.
- */
-const path = require('path');
-const glob = require('glob');
-
-const config = require('./webpack.config.common');
-
-config['mode'] = 'development';
-
-const allTestFiles = [...glob.sync('./src/**/*_test.js'), ...glob.sync('./src/**/*_test.ts')];
-const unitTestFiles = allTestFiles
- .filter((file) => !file.match('.*_component_test\\.(js|ts)$'))
- .filter((file) => !file.match('.*e2e.*'));
-config['entry'] = {
- tests: unitTestFiles,
-};
-
-config['output'] = {
- path: path.resolve(__dirname, 'dist/unit_test'),
- filename: 'bundle.js',
-};
-
-config['target'] = 'node';
-config['node'] = {
- __dirname: false,
-};
-
-module.exports = config;