Merge tm-dev-plus-aosp-without-vendor@8763363

Bug: 236760014
Merged-In: I794bcb9e9c922c642c3fbf0808fb60de4445ab65
Change-Id: Id292c36692b0020d772dc61b863f9cd3b39fe0c5
This commit is contained in:
Xin Li
2022-06-27 23:37:25 +00:00
121 changed files with 3758 additions and 2638 deletions

View File

@@ -60,7 +60,6 @@ android_sdk_repo_host {
"bcc_compat",
"d8",
"dexdump",
"libaapt2_jni",
"llvm-rs-cc",
"split-select",
"zipalign",
@@ -100,7 +99,6 @@ android_sdk_repo_host {
"bcc_compat",
"d8",
"dexdump",
"libaapt2_jni",
"libwinpthread-1",
"lld",
"llvm-rs-cc",
@@ -110,7 +108,6 @@ android_sdk_repo_host {
},
lib64: {
deps: [
"libaapt2_jni",
"libwinpthread-1",
],
},

View File

@@ -39,7 +39,7 @@ class Params(object):
self.CNT_NOPKG = 0
# DIR is the list of directories to scan in TOPDIR.
self.DIR = "frameworks libcore"
self.IGNORE_DIR = [ "hosttests", "tools", "tests", "samples" ]
self.IGNORE_DIR = [ "hosttests", "tools", "tests", "samples", "layoutlib" ]
# IGNORE is a list of namespaces to ignore. Must be java
# package definitions (e.g. "com.blah.foo.")
self.IGNORE = [ "sun.", "libcore.", "dalvik.",

View File

@@ -48,6 +48,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -187,8 +188,8 @@ public class Monkey {
/** Categories we are allowed to launch **/
private ArrayList<String> mMainCategories = new ArrayList<String>();
/** Applications we can switch to. */
private ArrayList<ComponentName> mMainApps = new ArrayList<ComponentName>();
/** Applications we can switch to, as well as their corresponding categories. */
private HashMap<ComponentName, String> mMainApps = new HashMap<>();
/** The delay between event inputs **/
long mThrottle = 0;
@@ -1073,7 +1074,8 @@ public class Monkey {
Logger.out.println("// + Using main activity " + r.activityInfo.name
+ " (from package " + packageName + ")");
}
mMainApps.add(new ComponentName(packageName, r.activityInfo.name));
mMainApps.put(
new ComponentName(packageName, r.activityInfo.name), category);
} else {
if (mVerbose >= 3) { // very very verbose
Logger.out.println("// - NOT USING main activity "

View File

@@ -27,12 +27,15 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.view.IWindowManager;
import java.util.HashMap;
/**
* monkey activity event
*/
public class MonkeyActivityEvent extends MonkeyEvent {
private ComponentName mApp;
long mAlarmTime = 0;
private HashMap<ComponentName, String> mMainApps = new HashMap<>();
public MonkeyActivityEvent(ComponentName app) {
super(EVENT_TYPE_ACTIVITY);
@@ -45,12 +48,23 @@ public class MonkeyActivityEvent extends MonkeyEvent {
mAlarmTime = arg;
}
public MonkeyActivityEvent(ComponentName app,
HashMap<ComponentName, String> MainApps) {
super(EVENT_TYPE_ACTIVITY);
mApp = app;
mMainApps = MainApps;
}
/**
* @return Intent for the new activity
*/
private Intent getEvent() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
if (mMainApps.containsKey(mApp)) {
intent.addCategory(mMainApps.get(mApp));
} else {
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
intent.setComponent(mApp);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
return intent;

View File

@@ -26,7 +26,8 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
/**
@@ -94,7 +95,7 @@ public class MonkeySourceRandom implements MonkeyEventSource {
* values after we read any optional values.
**/
private float[] mFactors = new float[FACTORZ_COUNT];
private List<ComponentName> mMainApps;
private HashMap<ComponentName, String> mMainApps;
private int mEventCount = 0; //total number of events generated so far
private MonkeyEventQueue mQ;
private Random mRandom;
@@ -119,7 +120,7 @@ public class MonkeySourceRandom implements MonkeyEventSource {
return KeyEvent.keyCodeFromString(keyName);
}
public MonkeySourceRandom(Random random, List<ComponentName> MainApps,
public MonkeySourceRandom(Random random, HashMap<ComponentName, String> MainApps,
long throttle, boolean randomizeThrottle, boolean permissionTargetSystem) {
// default values for random distributions
// note, these are straight percentages, to match user input (cmd line args)
@@ -430,8 +431,8 @@ public class MonkeySourceRandom implements MonkeyEventSource {
} else if (cls < mFactors[FACTOR_SYSOPS]) {
lastKey = SYS_KEYS[mRandom.nextInt(SYS_KEYS.length)];
} else if (cls < mFactors[FACTOR_APPSWITCH]) {
MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
MonkeyActivityEvent e = new MonkeyActivityEvent(new ArrayList<ComponentName>(mMainApps.keySet()).get(
mRandom.nextInt(mMainApps.size())), mMainApps);
mQ.addLast(e);
return;
} else if (cls < mFactors[FACTOR_FLIP]) {
@@ -479,8 +480,8 @@ public class MonkeySourceRandom implements MonkeyEventSource {
* generate an activity event
*/
public void generateActivity() {
MonkeyActivityEvent e = new MonkeyActivityEvent(mMainApps.get(
mRandom.nextInt(mMainApps.size())));
MonkeyActivityEvent e = new MonkeyActivityEvent(new ArrayList<ComponentName>(mMainApps.keySet()).get(
mRandom.nextInt(mMainApps.size())), mMainApps);
mQ.addLast(e);
}

View File

@@ -0,0 +1,75 @@
cmake_minimum_required(VERSION 3.6)
project("Android Resources")
set(ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../)
set(ROOT_OUT ${CMAKE_CURRENT_LIST_DIR}/cmake_out)
set(AAPT2_BASE ${ROOT}/frameworks/base/tools/aapt2)
set(IDMAP2_BASE ${ROOT}/frameworks/base/cmds/idmap2)
set(ANDROIDFW_BASE ${ROOT}/frameworks/base/libs/androidfw)
set(JNI_BASE ${ROOT}/frameworks/base/core/jni)
set(AAPT_BASE ${ROOT}/frameworks/base/tools/aapt)
# Set host arch
if(EXISTS ${AAPT2_BASE}/aapt2-x86_64-linux_glibc)
message(NOTICE, "Applying HOST_ARCH x86_64-linux_glibc")
set(HOST_ARCH x86_64-linux_glibc)
elseif(EXISTS ${AAPT2_BASE}/aapt2-x86-linux_glibc)
message(NOTICE, "Applying HOST_ARCH x86-linux_glibc")
set(HOST_ARCH x86-linux_glibc)
elseif(EXISTS ${AAPT2_BASE}/aapt2-x86_64-windows)
message(NOTICE, "Applying HOST_ARCH x86_64-windows")
set(HOST_ARCH x86_64-windows)
elseif(EXISTS ${AAPT2_BASE}/aapt2-x86-windows)
message(NOTICE, "Applying HOST_ARCH x86-windows")
set(HOST_ARCH x86-windows)
else()
message(NOTICE, "Applying default HOST_ARCH x86_64-linux_glibc")
set(HOST_ARCH x86_64-linux_glibc)
endif()
# Set target arch
if(EXISTS ${IDMAP2_BASE}/idmap2-arm64-android)
message(NOTICE, "Applying TARGET_ARCH arm64-android")
set(TARGET_ARCH arm64-android)
elseif(EXISTS ${IDMAP2_BASE}/idmap2-arm-android)
message(NOTICE, "Applying TARGET_ARCH arm-android")
set(TARGET_ARCH arm-android)
elseif(EXISTS ${IDMAP2_BASE}/idmap2-x86_64-linux_glibc)
message(NOTICE, "Applying TARGET_ARCH x86_64-linux_glibc")
set(TARGET_ARCH x86_64-linux_glibc)
elseif(EXISTS ${IDMAP2_BASE}/idmap2-x86-linux_glibc)
message(NOTICE, "Applying TARGET_ARCH x86-linux_glibc")
set(TARGET_ARCH x86-linux_glibc)
else()
message(NOTICE, "Applying default TARGET_ARCH arm64-android")
set(TARGET_ARCH arm64-android)
endif()
# aapt2
add_subdirectory(${AAPT2_BASE}/aapt2-${HOST_ARCH} ${ROOT_OUT}/aapt2)
add_subdirectory(${AAPT2_BASE}/aapt2_tests-${HOST_ARCH} ${ROOT_OUT}/aapt2_tests)
add_subdirectory(${AAPT2_BASE}/libaapt2-${HOST_ARCH} ${ROOT_OUT}/libaapt2)
# idmap2
add_subdirectory(${IDMAP2_BASE}/idmap2-${TARGET_ARCH} ${ROOT_OUT}/idmap2)
add_subdirectory(${IDMAP2_BASE}/idmap2d-${TARGET_ARCH} ${ROOT_OUT}/idmap2d)
add_subdirectory(${IDMAP2_BASE}/libidmap2-${TARGET_ARCH} ${ROOT_OUT}/libidmap2)
add_subdirectory(${IDMAP2_BASE}/libidmap2_protos-${TARGET_ARCH} ${ROOT_OUT}/libidmap2_protos)
add_subdirectory(${IDMAP2_BASE}/libidmap2daidl-${TARGET_ARCH} ${ROOT_OUT}/libidmap2daidl)
add_subdirectory(${IDMAP2_BASE}/idmap2_tests-${TARGET_ARCH} ${ROOT_OUT}/idmap2_tests)
# Android Runtime
add_subdirectory(${ANDROIDFW_BASE}/libandroidfw-${TARGET_ARCH} ${ROOT_OUT}/libandroidfw)
add_subdirectory(${ANDROIDFW_BASE}/libandroidfw_tests-${TARGET_ARCH} ${ROOT_OUT}/libandroidfw_tests)
add_subdirectory(${ANDROIDFW_BASE}/libandroidfw_benchmarks-${TARGET_ARCH} ${ROOT_OUT}/libandroidfw_benchmarks)
add_subdirectory(${ANDROIDFW_BASE}/libandroidfw_fuzzer_lib-${TARGET_ARCH} ${ROOT_OUT}/libandroidfw_fuzzer_lib)
add_subdirectory(${ANDROIDFW_BASE}/fuzz/resourcefile_fuzzer/resourcefile_fuzzer-${TARGET_ARCH}
${ROOT_OUT}/fuzz/resourcefile_fuzzer/resourcefile_fuzzer)
# JNI
add_subdirectory(${JNI_BASE}/libandroid_runtime-${TARGET_ARCH} ${ROOT_OUT}/jni)
# aapt
add_subdirectory(${AAPT_BASE}/aapt-${HOST_ARCH} ${ROOT_OUT}/aapt)
add_subdirectory(${AAPT_BASE}/libaapt-${HOST_ARCH} ${ROOT_OUT}/libaapt)
add_subdirectory(${AAPT_BASE}/libaapt_tests-${HOST_ARCH} ${ROOT_OUT}/libaapt_tests)

View File

@@ -1,6 +1,7 @@
cmake_minimum_required(VERSION 3.6)
project(native)
add_subdirectory(libs/gui/libgui-arm64-android)
add_subdirectory(libs/gui/tests/libgui_test-arm64-android)
add_subdirectory(libs/ui/libui-arm64-android)
add_subdirectory(libs/renderengine/librenderengine-arm64-android)
add_subdirectory(services/surfaceflinger/surfaceflinger-arm64-android)
@@ -19,3 +20,6 @@ add_subdirectory(services/surfaceflinger/tests/waitforvsync/test-waitforvsync-ar
add_subdirectory(services/surfaceflinger/tests/unittests/libsurfaceflinger_unittest-arm64-android)
add_subdirectory(services/surfaceflinger/libSurfaceFlingerProp-arm64-android)
add_subdirectory(services/surfaceflinger/sysprop/libSurfaceFlingerProperties-arm64-android)
add_subdirectory(services/surfaceflinger/fuzzer/surfaceflinger_fuzzer-arm64-android)
add_subdirectory(services/surfaceflinger/fuzzer/surfaceflinger_displayhardware_fuzzer-arm64-android)
add_subdirectory(services/surfaceflinger/fuzzer/surfaceflinger_scheduler_fuzzer-arm64-android)

View File

@@ -286,10 +286,19 @@
</intent-filter>
</activity>
<activity android:name=".app.KeepClearRects"
android:label="@string/activity_keep_clear">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
</activity>
<activity android:name=".app.PictureInPicture"
android:label="@string/activity_picture_in_picture"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.NoActionBar"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
@@ -298,41 +307,13 @@
</intent-filter>
</activity>
<activity android:name=".app.PictureInPictureAutoEnter"
android:label="@string/activity_picture_in_picture_auto_enter"
<activity android:name=".app.ContentPictureInPicture"
android:label="@string/activity_picture_in_picture"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.NoActionBar"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
</activity>
<activity android:name=".app.PictureInPictureSeamlessResize"
android:label="@string/activity_picture_in_picture_seamless_resize"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
</activity>
<activity android:name=".app.PictureInPictureSourceRectHint"
android:label="@string/activity_picture_in_picture_source_rect_hint"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.SAMPLE_CODE" />
</intent-filter>
"screenSize|smallestScreenSize|screenLayout|orientation">
</activity>
<activity android:name=".app.MaxAspectRatio$Square"

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"
android:fillColor="@android:color/white"/>
</vector>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<!--
Demonstrates Keep-Clear areas API.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top|start">
<TextView
android:id="@+id/keep_clear_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:preferKeepClear="true"
android:gravity="center_vertical|center_horizontal"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/keep_clear_property_set"
android:background="@color/transparent_red"/>
<Switch
android:id="@+id/set_prefer_keep_clear_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/keep_clear_view"
android:padding="@dimen/keep_clear_text_view_padding"
android:text="@string/keep_clear_set_prefer_keep_clear_toggle" />
<Switch
android:id="@+id/set_bottom_right_rectangle_keep_clear_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/set_prefer_keep_clear_toggle"
android:padding="@dimen/keep_clear_text_view_padding"
android:text="@string/keep_clear_set_bottom_right_rectangle_keep_clear_toggle" />
<TextView
android:id="@+id/keep_clear_view_bottom_right"
android:layout_width="wrap_content"
android:layout_height="@dimen/keep_clear_text_view_size"
android:gravity="center_vertical|center_horizontal"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/keep_clear_view_bottom_right"
android:background="@color/transparent_green"/>
</RelativeLayout>

View File

@@ -15,18 +15,100 @@
~ limitations under the License.
-->
<!-- Demonstrates implementation of picture-in-picture. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="4dp"
android:gravity="center_horizontal"
<!--
Demonstrates Picture-In-Picture with various configurations.
- Enter PiP with on-screen button
- Enter PiP by swiping up to home or tap on home button
- Toggle the auto enter PiP flag on and off
- Toggle the source rect hint on and off
- Toggle the seamless resize flag on and off
- Change the position of current and next source rect hint
- Tablet layout on foldables
- Enter content PiP
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<!-- layout params would be changed programmatically -->
<include layout="@layout/picture_in_picture_content" />
<ScrollView
android:id="@+id/control_group"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Switch
android:id="@+id/auto_pip_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_auto_pip_toggle" />
<Switch
android:id="@+id/source_rect_hint_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_source_rect_hint_toggle" />
<Switch
android:id="@+id/seamless_resize_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_seamless_resize_toggle" />
<RadioGroup
android:id="@+id/current_position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_current_position" />
<RadioButton
android:id="@+id/radio_current_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_position_start" />
<RadioButton
android:id="@+id/radio_current_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_position_end" />
</RadioGroup>
<Button
android:id="@+id/enter_pip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter_picture_in_picture" />
<Button
android:id="@+id/enter_content_pip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enter_content_pip" />
</LinearLayout>
</ScrollView>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/enter_pip"
android:text="@string/enter_picture_in_picture">
</Button>
</LinearLayout>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Demonstrates Picture-In-Picture with auto enter enabled. -->
<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">
<!-- layout params would be changed programmatically -->
<com.example.android.apis.view.FixedAspectRatioImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="@drawable/sample_1"
app:aspectRatio="16/9" />
<Switch android:id="@+id/source_rect_hint_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_source_rect_hint_toggle" />
<Button android:id="@+id/change_orientation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_change_orientation" />
</LinearLayout>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<com.example.android.apis.view.FixedAspectRatioImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="@drawable/sample_1"
app:aspectRatio="16/9" />

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Demonstrates Picture-In-Picture's Seamless Resize. -->
<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">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/activity_picture_in_picture_seamless_resize_demo_text"
android:textSize="18sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<Switch
android:id="@+id/seamless_resize_switch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/activity_picture_in_picture_seamless_resize_switch"
android:checked="false"
android:background="@android:color/white"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- Demonstrates Picture-In-Picture's Source Rect Hint usage. -->
<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">
<FrameLayout
android:id="@+id/frame_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/entry_source_btn">
<VideoView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"/>
</FrameLayout>
<Button
android:id="@+id/entry_source_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/exit_source_btn"/>
<Button
android:id="@+id/exit_source_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -30,6 +30,8 @@
<color name="solid_green">#f0f0</color>
<color name="solid_yellow">#ffffff00</color>
<color name="purply">#ff884488</color>
<color name="transparent_red">#0fff0000</color>
<color name="transparent_green">#0f00ff00</color>
<!-- A custom theme that is a variation on the light them with a different
background color. -->

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<!-- This resource is true if running under at least Honeycomb's
API level. The default value is false; an alternative value
for Honeycomb is true. -->
<dimen name="keep_clear_text_view_padding">8dp</dimen>
<!-- This resource is true if running under at least Honeycomb MR2's
API level. The default value is false; an alternative value
for Honeycomb MR2 is true. -->
<dimen name="keep_clear_text_view_size">100dp</dimen>
</resources>

View File

@@ -63,21 +63,34 @@
<string name="screen_orientation">Screen Orientation</string>
<string name="activity_picture_in_picture">App/Activity/Picture in Picture</string>
<string name="activity_picture_in_picture_auto_enter">App/Activity/Picture in Picture Auto Enter</string>
<string name="activity_picture_in_picture_source_rect_hint_toggle">Toggle source rect hint</string>
<string name="activity_picture_in_picture_seamless_resize">App/Activity/Picture in Picture Seamless Resize</string>
<string name="activity_picture_in_picture_source_rect_hint">App/Activity/Picture in Picture Source Rect Hint</string>
<string name="activity_picture_in_picture_source_rect_hint_current_position">Current position: %1$s</string>
<string name="activity_picture_in_picture_source_rect_hint_position_on_exit">Position on exit: %1$s</string>
<string name="activity_picture_in_picture_change_orientation">Change orientation</string>
<string name="activity_picture_in_picture_seamless_resize_switch">Turn on Seamless Resize (recommend off)</string>
<string name="activity_picture_in_picture_seamless_resize_demo_text">
The quick brown fox jumps over the lazy dog.
<string name="activity_picture_in_picture_auto_pip_toggle">Enable auto PiP</string>
<string name="activity_picture_in_picture_source_rect_hint_toggle">Enable source rect hint</string>
<string name="activity_picture_in_picture_seamless_resize_toggle">Enable seamless resize</string>
<string name="enter_content_pip">Enter content PiP</string>
<string name="enter_picture_in_picture">Manually enter PiP</string>
<string name="action_custom_close">Close PiP</string>
<string name="label_current_position">Current position</string>
<string name="label_exit_position">Exit position</string>
<string name="label_position_start">Start</string>
<string name="label_position_end">End</string>
<string name="activity_keep_clear">App/Activity/Keep Clear Rects</string>
<string name="keep_clear_property_set">
This view has android:preferKeepClear property set.
</string>
<string name="keep_clear_set_prefer_keep_clear_toggle">
Set the bottom right view as keep clear area, via setPreferKeepClear;
</string>
<string name="keep_clear_set_bottom_right_rectangle_keep_clear_toggle">
Set the bottom right view as keep clear area, via setPreferKeepClearRects.
</string>
<string name="keep_clear_view_bottom_right">
Use toggles to turn this into keep clear area.
</string>
<string name="activity_max_aspect_ratio_square">App/Activity/Max Aspect Ratio/1:1</string>
<string name="activity_max_aspect_ratio_16to9">App/Activity/Max Aspect Ratio/16:9</string>
<string name="activity_max_aspect_ratio_any">App/Activity/Max Aspect Ratio/Any</string>
<string name="enter_picture_in_picture">Enter picture-in-picture mode</string>
<string name="activity_translucent">App/Activity/Translucent</string>
<string name="translucent_background">Example of how you can make an

View File

@@ -17,46 +17,35 @@
package com.example.android.apis.app;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import android.widget.Switch;
import android.os.ResultReceiver;
import com.example.android.apis.R;
public class PictureInPictureSeamlessResize extends Activity {
private Switch mSwitchView;
public class ContentPictureInPicture extends Activity {
private ResultReceiver mOnStopReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActionBar().hide();
setContentView(R.layout.picture_in_picture_seamless_resize);
setContentView(R.layout.picture_in_picture_content);
mOnStopReceiver = getIntent().getParcelableExtra(PictureInPicture.KEY_ON_STOP_RECEIVER);
}
mSwitchView = findViewById(R.id.seamless_resize_switch);
mSwitchView.setOnCheckedChangeListener((v, isChecked) -> {
onSeamlessResizeCheckedChanged(isChecked);
});
onSeamlessResizeCheckedChanged(mSwitchView.isChecked());
@Override
protected void onStop() {
super.onStop();
if (mOnStopReceiver != null) {
mOnStopReceiver.send(0 /* resultCode */, Bundle.EMPTY);
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig) {
mSwitchView.setVisibility(isInPictureInPictureMode ? View.GONE : View.VISIBLE);
}
@Override
public boolean onPictureInPictureRequested() {
enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
return true;
}
private void onSeamlessResizeCheckedChanged(boolean checked) {
final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
.setSeamlessResizeEnabled(checked);
setPictureInPictureParams(builder.build());
if (!isInPictureInPictureMode) {
finish();
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.apis.app;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RelativeLayout;
import android.widget.Switch;
import com.example.android.apis.R;
import java.util.Arrays;
import java.util.List;
public class KeepClearRects extends Activity {
private static final String EXTRA_SET_PREFER_KEEP_CLEAR = "prefer_keep_clear";
private static final String EXTRA_SET_PREFER_KEEP_CLEAR_RECTS = "prefer_keep_clear_rects";
private RelativeLayout mRootView;
private View mKeepClearView;
private Switch mViewAsRestrictedKeepClearAreaToggle;
private Switch mBottomRightCornerKeepClearAreaToggle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.keep_clear_rects_activity);
// Find views
mRootView = findViewById(R.id.container);
mKeepClearView = findViewById(R.id.keep_clear_view_bottom_right);
mViewAsRestrictedKeepClearAreaToggle = findViewById(R.id.set_prefer_keep_clear_toggle);
mBottomRightCornerKeepClearAreaToggle = findViewById(
R.id.set_bottom_right_rectangle_keep_clear_toggle);
// Add listeners
mViewAsRestrictedKeepClearAreaToggle.setOnCheckedChangeListener(mOnToggleChangedListener);
mBottomRightCornerKeepClearAreaToggle.setOnCheckedChangeListener(
mBottomRightCornerToggleChangedListener);
// Get defaults
final Intent intent = getIntent();
mViewAsRestrictedKeepClearAreaToggle.setChecked(
intent.getBooleanExtra(EXTRA_SET_PREFER_KEEP_CLEAR, false));
mBottomRightCornerKeepClearAreaToggle.setChecked(
intent.getBooleanExtra(EXTRA_SET_PREFER_KEEP_CLEAR_RECTS, false));
}
private final CompoundButton.OnCheckedChangeListener mOnToggleChangedListener =
(v, isChecked) -> mKeepClearView.setPreferKeepClear(isChecked);
private final CompoundButton.OnCheckedChangeListener mBottomRightCornerToggleChangedListener =
(v, isChecked) -> {
if (isChecked) {
mRootView.setPreferKeepClearRects(
Arrays.asList(new Rect(
mKeepClearView.getLeft(),
mKeepClearView.getTop(),
mKeepClearView.getRight(),
mKeepClearView.getBottom())));
} else {
mRootView.setPreferKeepClearRects(List.of());
}
};
}

View File

@@ -16,27 +16,344 @@
package com.example.android.apis.app;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.widget.Button;
import android.os.Handler;
import android.os.Looper;
import android.os.ResultReceiver;
import android.util.Rational;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
import android.widget.Switch;
import com.example.android.apis.R;
public class PictureInPicture extends Activity {
import java.util.ArrayList;
import java.util.List;
private Button mEnterPip;
public class PictureInPicture extends Activity {
private static final String EXTRA_ENABLE_AUTO_PIP = "auto_pip";
private static final String EXTRA_ENABLE_SOURCE_RECT_HINT = "source_rect_hint";
private static final String EXTRA_ENABLE_SEAMLESS_RESIZE = "seamless_resize";
private static final String EXTRA_CURRENT_POSITION = "current_position";
private static final int TABLET_BREAK_POINT_DP = 700;
private static final String ACTION_CUSTOM_CLOSE = "demo.pip.custom_close";
private final BroadcastReceiver mRemoteActionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case ACTION_CUSTOM_CLOSE:
finish();
break;
}
}
};
public static final String KEY_ON_STOP_RECEIVER = "on_stop_receiver";
private final ResultReceiver mOnStopReceiver = new ResultReceiver(
new Handler(Looper.myLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
// Container activity for content-pip has stopped, replace the placeholder
// with actual content in this host activity.
mImageView.setImageResource(R.drawable.sample_1);
}
};
private final View.OnLayoutChangeListener mOnLayoutChangeListener =
(v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight, newBottom) -> {
updatePictureInPictureParams();
};
private final CompoundButton.OnCheckedChangeListener mOnToggleChangedListener =
(v, isChecked) -> updatePictureInPictureParams();
private final RadioGroup.OnCheckedChangeListener mOnPositionChangedListener =
(v, id) -> updateContentPosition(id);
private LinearLayout mContainer;
private ImageView mImageView;
private View mControlGroup;
private Switch mAutoPipToggle;
private Switch mSourceRectHintToggle;
private Switch mSeamlessResizeToggle;
private RadioGroup mCurrentPositionGroup;
private List<RemoteAction> mPipActions;
private RemoteAction mCloseAction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.picture_in_picture);
mEnterPip = (Button)findViewById(R.id.enter_pip);
mEnterPip.setOnClickListener((v) -> enterPictureInPictureMode());
// Find views
mContainer = findViewById(R.id.container);
mImageView = findViewById(R.id.image);
mControlGroup = findViewById(R.id.control_group);
mAutoPipToggle = findViewById(R.id.auto_pip_toggle);
mSourceRectHintToggle = findViewById(R.id.source_rect_hint_toggle);
mSeamlessResizeToggle = findViewById(R.id.seamless_resize_toggle);
mCurrentPositionGroup = findViewById(R.id.current_position);
// Attach listeners
mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener);
mAutoPipToggle.setOnCheckedChangeListener(mOnToggleChangedListener);
mSourceRectHintToggle.setOnCheckedChangeListener(mOnToggleChangedListener);
mSeamlessResizeToggle.setOnCheckedChangeListener(mOnToggleChangedListener);
mCurrentPositionGroup.setOnCheckedChangeListener(mOnPositionChangedListener);
findViewById(R.id.enter_pip_button).setOnClickListener(v -> enterPictureInPictureMode());
findViewById(R.id.enter_content_pip_button).setOnClickListener(v -> enterContentPip());
// Set defaults
final Intent intent = getIntent();
mAutoPipToggle.setChecked(intent.getBooleanExtra(EXTRA_ENABLE_AUTO_PIP, false));
mSourceRectHintToggle.setChecked(
intent.getBooleanExtra(EXTRA_ENABLE_SOURCE_RECT_HINT, false));
mSeamlessResizeToggle.setChecked(
intent.getBooleanExtra(EXTRA_ENABLE_SEAMLESS_RESIZE, false));
final int positionId = "end".equalsIgnoreCase(
intent.getStringExtra(EXTRA_CURRENT_POSITION))
? R.id.radio_current_end
: R.id.radio_current_start;
mCurrentPositionGroup.check(positionId);
updateLayout(getResources().getConfiguration());
}
@Override
protected void onStart() {
super.onStart();
setupPipActions();
}
@Override
protected void onUserLeaveHint() {
enterPictureInPictureMode();
// Only used when auto PiP is disabled. This is to simulate the behavior that an app
// supports regular PiP but not auto PiP.
if (!mAutoPipToggle.isChecked()) {
enterPictureInPictureMode();
}
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
updateLayout(newConfiguration);
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig) {
if (!isInPictureInPictureMode) {
// When it's about to exit PiP mode, always reset the mImageView position to start.
// If position is previously set to end, this should demonstrate the exit
// source rect hint behavior introduced in S.
mCurrentPositionGroup.check(R.id.radio_current_start);
}
}
@Override
protected void onStop() {
super.onStop();
unregisterReceiver(mRemoteActionReceiver);
}
/**
* This is what we expect most host Activity would do to trigger content PiP.
* - Get the bounds of the view to be transferred to content PiP
* - Construct the PictureInPictureParams with source rect hint and aspect ratio from bounds
* - Start the new content PiP container Activity with the ActivityOptions
*/
private void enterContentPip() {
final Intent intent = new Intent(this, ContentPictureInPicture.class);
intent.putExtra(KEY_ON_STOP_RECEIVER, mOnStopReceiver);
final Rect bounds = new Rect();
mImageView.getGlobalVisibleRect(bounds);
final PictureInPictureParams params = new PictureInPictureParams.Builder()
.setSourceRectHint(bounds)
.setAspectRatio(new Rational(bounds.width(), bounds.height()))
.build();
final ActivityOptions opts = ActivityOptions.makeLaunchIntoPip(params);
startActivity(intent, opts.toBundle());
// Swap the mImageView to placeholder content.
mImageView.setImageResource(R.drawable.black_box);
}
private void updateLayout(Configuration configuration) {
mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener);
final boolean isTablet = configuration.smallestScreenWidthDp >= TABLET_BREAK_POINT_DP;
final boolean isLandscape =
(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE);
final boolean isPictureInPicture = isInPictureInPictureMode();
if (isPictureInPicture) {
setupPictureInPictureLayout();
} else if (isTablet && isLandscape) {
setupTabletLandscapeLayout();
} else if (isLandscape) {
setupFullScreenLayout();
} else {
setupRegularLayout();
}
}
private void setupPipActions() {
final IntentFilter remoteActionFilter = new IntentFilter();
remoteActionFilter.addAction(ACTION_CUSTOM_CLOSE);
registerReceiver(mRemoteActionReceiver, remoteActionFilter);
final Intent intent = new Intent(ACTION_CUSTOM_CLOSE).setPackage(getPackageName());
mCloseAction = new RemoteAction(
Icon.createWithResource(this, R.drawable.ic_call_end),
getString(R.string.action_custom_close),
getString(R.string.action_custom_close),
PendingIntent.getBroadcast(this, 0 /* requestCode */, intent,
FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE));
// Add close action as a regular PiP action
mPipActions = new ArrayList<>(1);
mPipActions.add(mCloseAction);
}
private void setupPictureInPictureLayout() {
mControlGroup.setVisibility(View.GONE);
final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
imageLp.gravity = Gravity.NO_GRAVITY;
mImageView.setLayoutParams(imageLp);
}
private void setupTabletLandscapeLayout() {
mControlGroup.setVisibility(View.VISIBLE);
exitFullScreenMode();
final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
imageLp.gravity = Gravity.NO_GRAVITY;
enterTwoPaneMode(imageLp);
}
private void setupFullScreenLayout() {
mControlGroup.setVisibility(View.GONE);
enterFullScreenMode();
final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT);
imageLp.gravity = Gravity.CENTER_HORIZONTAL;
enterOnePaneMode(imageLp);
}
private void setupRegularLayout() {
mControlGroup.setVisibility(View.VISIBLE);
exitFullScreenMode();
final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
imageLp.gravity = Gravity.NO_GRAVITY;
enterOnePaneMode(imageLp);
}
private void enterOnePaneMode(LinearLayout.LayoutParams imageLp) {
mContainer.setOrientation(LinearLayout.VERTICAL);
final LinearLayout.LayoutParams controlLp =
(LinearLayout.LayoutParams) mControlGroup.getLayoutParams();
controlLp.width = LinearLayout.LayoutParams.MATCH_PARENT;
controlLp.height = 0;
controlLp.weight = 1;
mControlGroup.setLayoutParams(controlLp);
imageLp.weight = 0;
mImageView.setLayoutParams(imageLp);
}
private void enterTwoPaneMode(LinearLayout.LayoutParams imageLp) {
mContainer.setOrientation(LinearLayout.HORIZONTAL);
final LinearLayout.LayoutParams controlLp =
(LinearLayout.LayoutParams) mControlGroup.getLayoutParams();
controlLp.width = 0;
controlLp.height = LinearLayout.LayoutParams.MATCH_PARENT;
controlLp.weight = 1;
mControlGroup.setLayoutParams(controlLp);
imageLp.width = 0;
imageLp.height = LinearLayout.LayoutParams.WRAP_CONTENT;
imageLp.weight = 1;
mImageView.setLayoutParams(imageLp);
}
private void enterFullScreenMode() {
// TODO(b/188001699) switch to use insets controller once the bug is fixed.
final View decorView = getWindow().getDecorView();
final int systemUiNavigationBarFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
| systemUiNavigationBarFlags);
}
private void exitFullScreenMode() {
final View decorView = getWindow().getDecorView();
final int systemUiNavigationBarFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
& ~systemUiNavigationBarFlags);
}
private void updatePictureInPictureParams() {
mImageView.removeOnLayoutChangeListener(mOnLayoutChangeListener);
// do not bother PictureInPictureParams update when it's already in pip mode.
if (isInPictureInPictureMode()) return;
final Rect imageViewRect = new Rect();
mImageView.getGlobalVisibleRect(imageViewRect);
// bail early if mImageView has not been measured yet
if (imageViewRect.isEmpty()) return;
final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
.setAutoEnterEnabled(mAutoPipToggle.isChecked())
.setSourceRectHint(mSourceRectHintToggle.isChecked()
? new Rect(imageViewRect) : null)
.setSeamlessResizeEnabled(mSeamlessResizeToggle.isChecked())
.setAspectRatio(new Rational(imageViewRect.width(), imageViewRect.height()))
.setActions(mPipActions)
.setCloseAction(mCloseAction);
setPictureInPictureParams(builder.build());
}
private void updateContentPosition(int checkedId) {
mContainer.removeAllViews();
mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener);
if (checkedId == R.id.radio_current_start) {
mContainer.addView(mImageView, 0);
mContainer.addView(mControlGroup, 1);
} else {
mContainer.addView(mControlGroup, 0);
mContainer.addView(mImageView, 1);
}
}
}

View File

@@ -1,138 +0,0 @@
/*
* 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.
*/
package com.example.android.apis.app;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Rational;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.Switch;
import com.example.android.apis.R;
public class PictureInPictureAutoEnter extends Activity {
private final View.OnLayoutChangeListener mOnLayoutChangeListener =
(v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight, newBottom) -> {
updatePictureInPictureParams();
};
private final CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener =
(v, isChecked) -> updatePictureInPictureParams();
private View mImageView;
private View mButtonView;
private Switch mSwitchView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.picture_in_picture_auto_enter);
mImageView = findViewById(R.id.image);
mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener);
mButtonView = findViewById(R.id.change_orientation);
mButtonView.setOnClickListener((v) -> {
final int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
});
mSwitchView = findViewById(R.id.source_rect_hint_toggle);
mSwitchView.setOnCheckedChangeListener(mOnCheckedChangeListener);
// there is a bug that setSourceRectHint(null) does not clear the source rect hint
// once there is a non-null source rect hint ever been set. set this to false by default
// therefore this demo activity can be used for testing autoEnterPip behavior without
// source rect hint when launched for the first time.
mSwitchView.setChecked(false);
updateLayout(getResources().getConfiguration());
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
updateLayout(newConfiguration);
}
private void updateLayout(Configuration configuration) {
final boolean isLandscape =
(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE);
final boolean isPictureInPicture = isInPictureInPictureMode();
mButtonView.setVisibility((isPictureInPicture || isLandscape) ? View.GONE : View.VISIBLE);
mSwitchView.setVisibility((isPictureInPicture || isLandscape) ? View.GONE: View.VISIBLE);
final LinearLayout.LayoutParams layoutParams;
if (isPictureInPicture) {
layoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
layoutParams.gravity = Gravity.NO_GRAVITY;
} else {
// Toggle the fullscreen mode as well.
// TODO(b/188001699) switch to use insets controller once the bug is fixed.
final View decorView = getWindow().getDecorView();
final int systemUiNavigationBarFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
if (isLandscape) {
layoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT);
layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
| systemUiNavigationBarFlags);
} else {
layoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.NO_GRAVITY;
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
& ~systemUiNavigationBarFlags);
}
}
mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener);
mImageView.setLayoutParams(layoutParams);
}
private void updatePictureInPictureParams() {
mImageView.removeOnLayoutChangeListener(mOnLayoutChangeListener);
// do not bother PictureInPictureParams update when it's already in pip mode.
if (isInPictureInPictureMode()) return;
final Rect imageViewRect = new Rect();
mImageView.getGlobalVisibleRect(imageViewRect);
// bail early if mImageView has not been measured yet
if (imageViewRect.isEmpty()) return;
final Rect sourceRectHint = mSwitchView.isChecked() ? new Rect(imageViewRect) : null;
final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder()
.setAutoEnterEnabled(true)
.setAspectRatio(new Rational(imageViewRect.width(), imageViewRect.height()))
.setSourceRectHint(sourceRectHint);
setPictureInPictureParams(builder.build());
}
}

View File

@@ -1,156 +0,0 @@
/*
* 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.
*/
package com.example.android.apis.app;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.util.Rational;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.VideoView;
import com.example.android.apis.R;
public class PictureInPictureSourceRectHint extends Activity {
private FrameLayout mFrameLayout;
private VideoView mVideoView;
private Button mEntryButton;
private Button mExitButton;
private int mPositionOnEntry;
private int mPositionOnExit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getActionBar().hide();
setContentView(R.layout.picture_in_picture_source_rect_hint);
mFrameLayout = findViewById(R.id.frame_layout);
mVideoView = findViewById(R.id.video_view);
mEntryButton = findViewById(R.id.entry_source_btn);
mExitButton = findViewById(R.id.exit_source_btn);
initPlayer(Uri.parse("android.resource://" + getPackageName() +
"/" + R.raw.videoviewdemo));
setEntryState(Gravity.TOP);
setExitState(Gravity.TOP);
mEntryButton.setOnClickListener(v -> {
// Toggle the position and update the views.
setEntryState(mPositionOnEntry == Gravity.TOP ? Gravity.BOTTOM : Gravity.TOP);
});
mExitButton.setOnClickListener(v -> {
// Toggle the position and update the views.
setExitState(mPositionOnExit == Gravity.TOP ? Gravity.BOTTOM : Gravity.TOP);
});
mVideoView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (left == oldLeft && right == oldRight && top == oldTop
&& bottom == oldBottom) return;
setPictureInPictureParams(new PictureInPictureParams.Builder()
.setSourceRectHint(getVideoRectHint())
.build());
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mVideoView.stopPlayback();
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
Configuration newConfig) {
mEntryButton.setVisibility(isInPictureInPictureMode ? View.GONE : View.VISIBLE);
mExitButton.setVisibility(isInPictureInPictureMode ? View.GONE : View.VISIBLE);
final FrameLayout.LayoutParams params;
if (isInPictureInPictureMode) {
// In PIP mode the video should take up the full width and height.
params = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
} else {
// Exiting PIP, return the video to its original size and place it to the preferred
// gravity selected before entering PIP mode.
params = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
params.gravity = mPositionOnExit;
// The "exit" gravity becomes the current "entry" gravity, update it and its views.
setEntryState(mPositionOnExit);
}
mVideoView.setLayoutParams(params);
}
@Override
public boolean onPictureInPictureRequested() {
final Rect hint = getVideoRectHint();
enterPictureInPictureMode(new PictureInPictureParams.Builder()
.setSourceRectHint(hint)
.setAspectRatio(new Rational(hint.width(), hint.height()))
.build());
return true;
}
private void initPlayer(Uri uri) {
mVideoView.setVideoURI(uri);
mVideoView.requestFocus();
mVideoView.setOnPreparedListener(mp -> {
mp.setLooping(true);
mVideoView.start();
});
}
private Rect getVideoRectHint() {
final Rect hint = new Rect();
mVideoView.getGlobalVisibleRect(hint);
return hint;
}
private void setEntryState(int gravity) {
mPositionOnEntry = gravity;
mEntryButton.setText(getString(
R.string.activity_picture_in_picture_source_rect_hint_current_position,
getGravityName(mPositionOnEntry)));
final FrameLayout.LayoutParams p = (FrameLayout.LayoutParams) mVideoView.getLayoutParams();
p.gravity = mPositionOnEntry;
mVideoView.setLayoutParams(p);
}
private void setExitState(int gravity) {
mPositionOnExit = gravity;
mExitButton.setText(getString(
R.string.activity_picture_in_picture_source_rect_hint_position_on_exit,
getGravityName(mPositionOnExit)));
}
private String getGravityName(int gravity) {
return gravity == Gravity.TOP ? "TOP" : "BOTTOM";
}
}

View File

@@ -58,7 +58,6 @@ public class FixedAspectRatioImageView extends ImageView {
height = MeasureSpec.getSize(heightMeasureSpec);
width = (int) (height * mAspectRatio.floatValue());
}
android.util.Log.d("DebugMe", "onMeasure w=" + width + " h=" + height);
setMeasuredDimension(width, height);
}
}

View File

@@ -23,6 +23,7 @@ import android.service.autofill.FillCallback;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.InlinePresentation;
import android.service.autofill.Presentations;
import android.service.autofill.SaveCallback;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
@@ -95,14 +96,17 @@ public class InlineFillService extends AutofillService {
InlinePresentation inlinePresentation =
InlineRequestHelper.maybeCreateInlineAuthenticationResponse(context,
inlineRequest);
final Presentations.Builder fieldPresentationsBuilder =
new Presentations.Builder();
fieldPresentationsBuilder.setMenuPresentation(presentation);
fieldPresentationsBuilder.setInlinePresentation(inlinePresentation);
response = new FillResponse.Builder()
.setAuthentication(ids, authentication, presentation, inlinePresentation)
.setAuthentication(ids, authentication, fieldPresentationsBuilder.build())
.build();
} else {
response = createResponse(this, fields, maxSuggestionsCount, mAuthenticateDatasets,
inlineRequest);
}
callback.onSuccess(response);
}
@@ -130,8 +134,13 @@ public class InlineFillService extends AutofillService {
response.addDataset(InlineRequestHelper.createInlineActionDataset(context, fields,
inlineRequest.get(), R.drawable.ic_settings));
}
// 3. Add fill dialog
RemoteViews dialogPresentation =
ResponseHelper.newDatasetPresentation(packageName, "Dialog Header");
response.setDialogHeader(dialogPresentation);
response.setFillDialogTriggerIds(fields.valueAt(0), fields.valueAt(1));
// 3.Add save info
// 4.Add save info
Collection<AutofillId> ids = fields.values();
AutofillId[] requiredIds = new AutofillId[ids.size()];
ids.toArray(requiredIds);
@@ -139,7 +148,7 @@ public class InlineFillService extends AutofillService {
// We're simple, so we're generic
new SaveInfo.Builder(SaveInfo.SAVE_DATA_TYPE_GENERIC, requiredIds).build());
// 4.Profit!
// 5.Profit!
return response.build();
}

View File

@@ -21,7 +21,9 @@ import static com.example.android.inlinefillservice.InlineFillService.TAG;
import android.content.Context;
import android.content.IntentSender;
import android.service.autofill.Dataset;
import android.service.autofill.Field;
import android.service.autofill.InlinePresentation;
import android.service.autofill.Presentations;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
@@ -51,21 +53,31 @@ class ResponseHelper {
Log.d(TAG, "hint: " + hint);
final String displayValue = hint.contains("password") ? "password for #" + (index + 1)
: value;
final RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
// Add Inline Suggestion required info.
Field.Builder fieldBuilder = new Field.Builder();
fieldBuilder.setValue(AutofillValue.forText(value));
// Set presentation
final Presentations.Builder fieldPresentationsBuilder =
new Presentations.Builder();
// Dropdown presentation
final RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
fieldPresentationsBuilder.setMenuPresentation(presentation);
// Inline presentation
if (inlineRequest.isPresent()) {
Log.d(TAG, "Found InlineSuggestionsRequest in FillRequest: " + inlineRequest);
final InlinePresentation inlinePresentation =
InlineRequestHelper.createInlineDataset(context, inlineRequest.get(),
displayValue, index);
dataset.setValue(id, AutofillValue.forText(value), presentation,
inlinePresentation);
} else {
dataset.setValue(id, AutofillValue.forText(value), presentation);
InlineRequestHelper.createInlineDataset(context, inlineRequest.get(),
displayValue, index);
fieldPresentationsBuilder.setInlinePresentation(inlinePresentation);
}
}
// Dialog presentation
RemoteViews dialogPresentation =
newDatasetPresentation(packageName, "Dialog Presentation " + (index + 1));
fieldPresentationsBuilder.setDialogPresentation(dialogPresentation);
fieldBuilder.setPresentations(fieldPresentationsBuilder.build());
dataset.setField(id, fieldBuilder.build());
}
return dataset.build();
}
@@ -84,16 +96,28 @@ class ResponseHelper {
IntentSender authentication =
AuthActivity.newIntentSenderForDataset(context, unlockedDataset);
RemoteViews presentation = newDatasetPresentation(packageName, displayValue);
Field.Builder fieldBuilder = new Field.Builder();
fieldBuilder.setValue(AutofillValue.forText(value));
// Dropdown presentation
final Presentations.Builder fieldPresentationsBuilder =
new Presentations.Builder();
fieldPresentationsBuilder.setMenuPresentation(presentation);
// Inline presentation
if (inlineRequest.isPresent()) {
final InlinePresentation inlinePresentation =
InlineRequestHelper.createInlineDataset(context, inlineRequest.get(),
displayValue, index);
lockedDataset.setValue(id, null, presentation, inlinePresentation)
.setAuthentication(authentication);
} else {
lockedDataset.setValue(id, null, presentation)
.setAuthentication(authentication);
fieldPresentationsBuilder.setInlinePresentation(inlinePresentation);
}
// Dialog presentation
RemoteViews dialogPresentation =
newDatasetPresentation(packageName, "Dialog Presentation " + (index + 1));
fieldPresentationsBuilder.setDialogPresentation(dialogPresentation);
fieldBuilder.setPresentations(fieldPresentationsBuilder.build());
lockedDataset.setField(id, fieldBuilder.build());
lockedDataset.setId(null).setAuthentication(authentication);
}
return lockedDataset.build();
}

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.multiclientinputmethod">
<application android:label="MultiClientInputMethod">
<service android:name=".MultiClientInputMethod"
android:label="MultiClientInputMethod"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.inputmethodservice.MultiClientInputMethodService"/>
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method"/>
</service>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<android.inputmethodservice.KeyboardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 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.
*/
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string-array name="config_inputDisplayToImeDisplay">
<!--
The MultiClientInputMethod will use the same display for the IME window by default.
But, if you want to use the separate display for the IME window, consider to define item of
'config_inputDisplayToImeDisplay'. The each item is a slash-separated (/) pair of the display
the uniqueIds. The first is the uniqueId of the display where the input happens and the second
is the unqiueId of the display where the IME window will be shown.
FYI, you can find the uniqueId of displays in "dumpsys display".
E.g. If you have two displays 19261083906282752, local:19260422155234049 and you want to use
local:19260422155234049 as the IME window for the input at the display local:19261083906282752,
then the config item will be:
<item>local:19261083906282752/local:19260422155234049</item>
E.g. The display of ActivityView has the unique id of the form of
'virtual:' + package_name + ',' + ownerUid + ',' + 'ActivityViewVirtualDisplay@'
+ hashCode + ',' + displayIndex.
We can use the following regular expression to match it:
<item>virtual:com.android.car.carlauncher,\\d+,ActivityViewVirtualDisplay@\\d+,\\d+/local:19260422155234049</item>
-->
</string-array>
</resources>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<input-method xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p"
android:horizontalGap="0px"
android:verticalGap="0px"
android:keyHeight="50dip"
>
<Row>
<Key android:codes="113" android:keyLabel="q" android:keyEdgeFlags="left"/>
<Key android:codes="119" android:keyLabel="w"/>
<Key android:codes="101" android:keyLabel="e"/>
<Key android:codes="114" android:keyLabel="r"/>
<Key android:codes="116" android:keyLabel="t"/>
<Key android:codes="121" android:keyLabel="y"/>
<Key android:codes="117" android:keyLabel="u"/>
<Key android:codes="105" android:keyLabel="i"/>
<Key android:codes="111" android:keyLabel="o"/>
<Key android:codes="112" android:keyLabel="p" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="97" android:keyLabel="a" android:horizontalGap="5%p"
android:keyEdgeFlags="left"/>
<Key android:codes="115" android:keyLabel="s"/>
<Key android:codes="100" android:keyLabel="d"/>
<Key android:codes="102" android:keyLabel="f"/>
<Key android:codes="103" android:keyLabel="g"/>
<Key android:codes="104" android:keyLabel="h"/>
<Key android:codes="106" android:keyLabel="j"/>
<Key android:codes="107" android:keyLabel="k"/>
<Key android:codes="108" android:keyLabel="l" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="-1" android:keyIcon="@drawable/sym_keyboard_shift"
android:keyWidth="15%p" android:isModifier="true"
android:isSticky="true" android:keyEdgeFlags="left"/>
<Key android:codes="122" android:keyLabel="z"/>
<Key android:codes="120" android:keyLabel="x"/>
<Key android:codes="99" android:keyLabel="c"/>
<Key android:codes="118" android:keyLabel="v"/>
<Key android:codes="98" android:keyLabel="b"/>
<Key android:codes="110" android:keyLabel="n"/>
<Key android:codes="109" android:keyLabel="m"/>
<Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete"
android:keyWidth="15%p" android:keyEdgeFlags="right"
android:isRepeatable="true"/>
</Row>
<Row android:rowEdgeFlags="bottom">
<Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done"
android:keyWidth="15%p" android:keyEdgeFlags="left"/>
<Key android:codes="-2" android:keyLabel="123" android:keyWidth="10%p"/>
<Key android:codes="32" android:keyIcon="@drawable/sym_keyboard_space"
android:keyWidth="40%p" android:isRepeatable="true"/>
<Key android:codes="46,44" android:keyLabel=". ,"
android:keyWidth="15%p"/>
<Key android:codes="10" android:keyIcon="@drawable/sym_keyboard_return"
android:keyWidth="20%p" android:keyEdgeFlags="right"/>
</Row>
</Keyboard>

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p"
android:horizontalGap="0px"
android:verticalGap="0px"
android:keyHeight="50dip"
>
<Row>
<Key android:codes="49" android:keyLabel="1" android:keyEdgeFlags="left"/>
<Key android:codes="50" android:keyLabel="2"/>
<Key android:codes="51" android:keyLabel="3"/>
<Key android:codes="52" android:keyLabel="4"/>
<Key android:codes="53" android:keyLabel="5"/>
<Key android:codes="54" android:keyLabel="6"/>
<Key android:codes="55" android:keyLabel="7"/>
<Key android:codes="56" android:keyLabel="8"/>
<Key android:codes="57" android:keyLabel="9"/>
<Key android:codes="48" android:keyLabel="0" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="64" android:keyLabel="\@" android:keyEdgeFlags="left"/>
<Key android:codes="35" android:keyLabel="\#"/>
<Key android:codes="36" android:keyLabel="$"/>
<Key android:codes="37" android:keyLabel="%"/>
<Key android:codes="38" android:keyLabel="&amp;"/>
<Key android:codes="42" android:keyLabel="*"/>
<Key android:codes="45" android:keyLabel="-"/>
<Key android:codes="61" android:keyLabel="="/>
<Key android:codes="40" android:keyLabel="("/>
<Key android:codes="41" android:keyLabel=")" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="-1" android:keyIcon="@drawable/sym_keyboard_shift"
android:keyWidth="15%p" android:isModifier="true"
android:isSticky="true" android:keyEdgeFlags="left"/>
<Key android:codes="33" android:keyLabel="!" />
<Key android:codes="34" android:keyLabel="&quot;"/>
<Key android:codes="39" android:keyLabel="\'"/>
<Key android:codes="58" android:keyLabel=":"/>
<Key android:codes="59" android:keyLabel=";"/>
<Key android:codes="47" android:keyLabel="/" />
<Key android:codes="63" android:keyLabel="\?"/>
<Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete"
android:keyWidth="15%p" android:keyEdgeFlags="right"
android:isRepeatable="true"/>
</Row>
<Row android:rowEdgeFlags="bottom">
<Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done"
android:keyWidth="15%p" android:keyEdgeFlags="left"/>
<Key android:codes="-2" android:keyLabel="ABC" android:keyWidth="10%p"/>
<Key android:codes="32" android:keyIcon="@drawable/sym_keyboard_space"
android:keyWidth="40%p" android:isRepeatable="true"/>
<Key android:codes="46,44" android:keyLabel=". ,"
android:keyWidth="15%p"/>
<Key android:codes="10" android:keyIcon="@drawable/sym_keyboard_return"
android:keyWidth="20%p" android:keyEdgeFlags="right"/>
</Row>
</Keyboard>

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="10%p"
android:horizontalGap="0px"
android:verticalGap="0px"
android:keyHeight="50dip"
>
<Row>
<Key android:codes="126" android:keyLabel="~" android:keyEdgeFlags="left"/>
<Key android:codes="177" android:keyLabel="±"/>
<Key android:codes="215" android:keyLabel="×"/>
<Key android:codes="247" android:keyLabel="÷"/>
<Key android:codes="8226" android:keyLabel="•"/>
<Key android:codes="176" android:keyLabel="°"/>
<Key android:codes="96" android:keyLabel="`"/>
<Key android:codes="180" android:keyLabel="´"/>
<Key android:codes="123" android:keyLabel="{"/>
<Key android:codes="125" android:keyLabel="}" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="169" android:keyLabel="©" android:keyEdgeFlags="left"/>
<Key android:codes="163" android:keyLabel="£"/>
<Key android:codes="8364" android:keyLabel="€"/>
<Key android:codes="94" android:keyLabel="^"/>
<Key android:codes="174" android:keyLabel="®"/>
<Key android:codes="165" android:keyLabel="¥"/>
<Key android:codes="95" android:keyLabel="_"/>
<Key android:codes="43" android:keyLabel="+"/>
<Key android:codes="91" android:keyLabel="["/>
<Key android:codes="93" android:keyLabel="]" android:keyEdgeFlags="right"/>
</Row>
<Row>
<Key android:codes="-1" android:keyIcon="@drawable/sym_keyboard_shift"
android:keyWidth="15%p" android:isModifier="true"
android:isSticky="true" android:keyEdgeFlags="left"/>
<Key android:codes="161" android:keyLabel="¡" />
<Key android:codes="60" android:keyLabel="&lt;"/>
<Key android:codes="62" android:keyLabel="&gt;"/>
<Key android:codes="162" android:keyLabel="¢"/>
<Key android:codes="124" android:keyLabel="|"/>
<Key android:codes="92" android:keyLabel="\\" />
<Key android:codes="191" android:keyLabel="¿"/>
<Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete"
android:keyWidth="15%p" android:keyEdgeFlags="right"
android:isRepeatable="true"/>
</Row>
<Row android:rowEdgeFlags="bottom">
<Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done"
android:keyWidth="15%p" android:keyEdgeFlags="left"/>
<Key android:codes="-2" android:keyLabel="ABC" android:keyWidth="10%p"/>
<Key android:codes="32" android:keyIcon="@drawable/sym_keyboard_space"
android:keyWidth="40%p" android:isRepeatable="true"/>
<Key android:codes="46,44" android:keyLabel=". ,"
android:keyWidth="15%p"/>
<Key android:codes="10" android:keyIcon="@drawable/sym_keyboard_return"
android:keyWidth="20%p" android:keyEdgeFlags="right"/>
</Row>
</Keyboard>

View File

@@ -1,262 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
import android.os.Bundle;
import android.os.Looper;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
final class ClientCallbackImpl implements MultiClientInputMethodServiceDelegate.ClientCallback {
private static final String TAG = "ClientCallbackImpl";
private static final boolean DEBUG = false;
private final MultiClientInputMethodServiceDelegate mDelegate;
private final SoftInputWindowManager mSoftInputWindowManager;
private final int mClientId;
private final int mUid;
private final int mPid;
private final int mSelfReportedDisplayId;
private final KeyEvent.DispatcherState mDispatcherState;
private final Looper mLooper;
private final MultiClientInputMethod mInputMethod;
ClientCallbackImpl(MultiClientInputMethod inputMethod,
MultiClientInputMethodServiceDelegate delegate,
SoftInputWindowManager softInputWindowManager, int clientId, int uid, int pid,
int selfReportedDisplayId) {
mInputMethod = inputMethod;
mDelegate = delegate;
mSoftInputWindowManager = softInputWindowManager;
mClientId = clientId;
mUid = uid;
mPid = pid;
mSelfReportedDisplayId = selfReportedDisplayId;
mDispatcherState = new KeyEvent.DispatcherState();
// For simplicity, we use the main looper for this sample.
// To use other looper thread, make sure that the IME Window also runs on the same looper
// and introduce an appropriate synchronization mechanism instead of directly accessing
// MultiClientInputMethod#mDisplayToLastClientId.
mLooper = Looper.getMainLooper();
}
KeyEvent.DispatcherState getDispatcherState() {
return mDispatcherState;
}
Looper getLooper() {
return mLooper;
}
@Override
public void onAppPrivateCommand(String action, Bundle data) {
}
@Override
public void onDisplayCompletions(CompletionInfo[] completions) {
}
@Override
public void onFinishSession() {
if (DEBUG) {
Log.v(TAG, "onFinishSession clientId=" + mClientId);
}
final SoftInputWindow window =
mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
if (window == null) {
return;
}
// SoftInputWindow also needs to be cleaned up when this IME client is still associated with
// it.
if (mClientId == window.getClientId()) {
window.onFinishClient();
}
}
@Override
public void onHideSoftInput(int flags, ResultReceiver resultReceiver) {
if (DEBUG) {
Log.v(TAG, "onHideSoftInput clientId=" + mClientId + " flags=" + flags);
}
final SoftInputWindow window =
mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
if (window == null) {
return;
}
// Seems that the Launcher3 has a bug to call onHideSoftInput() too early so we cannot
// enforce clientId check yet.
// TODO: Check clientId like we do so for onShowSoftInput().
window.hide();
}
@Override
public void onShowSoftInput(int flags, ResultReceiver resultReceiver) {
if (DEBUG) {
Log.v(TAG, "onShowSoftInput clientId=" + mClientId + " flags=" + flags);
}
final SoftInputWindow window =
mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
if (window == null) {
return;
}
if (mClientId != window.getClientId()) {
Log.w(TAG, "onShowSoftInput() from a background client is ignored."
+ " windowClientId=" + window.getClientId()
+ " clientId=" + mClientId);
return;
}
window.show();
}
@Override
public void onStartInputOrWindowGainedFocus(InputConnection inputConnection,
EditorInfo editorInfo, int startInputFlags, int softInputMode, int targetWindowHandle) {
if (DEBUG) {
Log.v(TAG, "onStartInputOrWindowGainedFocus clientId=" + mClientId
+ " editorInfo=" + editorInfo
+ " startInputFlags="
+ InputMethodDebug.startInputFlagsToString(startInputFlags)
+ " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
+ " targetWindowHandle=" + targetWindowHandle);
}
final int state = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
final boolean forwardNavigation =
(softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
final SoftInputWindow window =
mSoftInputWindowManager.getOrCreateSoftInputWindow(mSelfReportedDisplayId);
if (window == null) {
return;
}
if (window.getTargetWindowHandle() != targetWindowHandle) {
// Target window has changed. Report new IME target window to the system.
mDelegate.reportImeWindowTarget(
mClientId, targetWindowHandle, window.getWindow().getAttributes().token);
}
final int lastClientId = mInputMethod.mDisplayToLastClientId.get(mSelfReportedDisplayId);
if (lastClientId != mClientId) {
// deactivate previous client and activate current.
mDelegate.setActive(lastClientId, false /* active */);
mDelegate.setActive(mClientId, true /* active */);
}
if (inputConnection == null || editorInfo == null) {
// Placeholder InputConnection case.
if (window.getClientId() == mClientId) {
// Special hack for temporary focus changes (e.g. notification shade).
// If we have already established a connection to this client, and if a placeholder
// InputConnection is notified, just ignore this event.
} else {
window.onDummyStartInput(mClientId, targetWindowHandle);
}
} else {
window.onStartInput(mClientId, targetWindowHandle, inputConnection);
}
switch (state) {
case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
if (forwardNavigation) {
window.show();
}
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
window.show();
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
if (forwardNavigation) {
window.hide();
}
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
window.hide();
break;
}
mInputMethod.mDisplayToLastClientId.put(mSelfReportedDisplayId, mClientId);
}
@Override
public void onUpdateCursorAnchorInfo(CursorAnchorInfo info) {
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd,
int candidatesStart, int candidatesEnd) {
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (DEBUG) {
Log.v(TAG, "onKeyDown clientId=" + mClientId + " keyCode=" + keyCode
+ " event=" + event);
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
final SoftInputWindow window =
mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
if (window != null && window.isShowing()) {
event.startTracking();
return true;
}
}
return false;
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return false;
}
@Override
public boolean onKeyMultiple(int keyCode, KeyEvent event) {
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (DEBUG) {
Log.v(TAG, "onKeyUp clientId=" + mClientId + "keyCode=" + keyCode
+ " event=" + event);
}
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
final SoftInputWindow window =
mSoftInputWindowManager.getSoftInputWindow(mSelfReportedDisplayId);
if (window != null && window.isShowing()) {
window.hide();
return true;
}
}
return false;
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
return false;
}
}

View File

@@ -1,119 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.view.WindowManager;
import com.android.internal.inputmethod.StartInputFlags;
import java.util.StringJoiner;
/**
* Provides useful methods for debugging.
*/
final class InputMethodDebug {
/**
* Not intended to be instantiated.
*/
private InputMethodDebug() {
}
/**
* Converts soft input flags to {@link String} for debug logging.
*
* @param softInputMode integer constant for soft input flags.
* @return {@link String} message corresponds for the given {@code softInputMode}.
*/
public static String softInputModeToString(int softInputMode) {
final StringJoiner joiner = new StringJoiner("|");
final int state = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
final int adjust = softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
final boolean isForwardNav =
(softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
switch (state) {
case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
joiner.add("STATE_UNSPECIFIED");
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
joiner.add("STATE_UNCHANGED");
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
joiner.add("STATE_HIDDEN");
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
joiner.add("STATE_ALWAYS_HIDDEN");
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
joiner.add("STATE_VISIBLE");
break;
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
joiner.add("STATE_ALWAYS_VISIBLE");
break;
default:
joiner.add("STATE_UNKNOWN(" + state + ")");
break;
}
switch (adjust) {
case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED:
joiner.add("ADJUST_UNSPECIFIED");
break;
case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE:
joiner.add("ADJUST_RESIZE");
break;
case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN:
joiner.add("ADJUST_PAN");
break;
case WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING:
joiner.add("ADJUST_NOTHING");
break;
default:
joiner.add("ADJUST_UNKNOWN(" + adjust + ")");
break;
}
if (isForwardNav) {
// This is a special bit that is set by the system only during the window navigation.
joiner.add("IS_FORWARD_NAVIGATION");
}
return joiner.setEmptyValue("(none)").toString();
}
/**
* Converts start input flags to {@link String} for debug logging.
*
* @param startInputFlags integer constant for start input flags.
* @return {@link String} message corresponds for the given {@code startInputFlags}.
*/
public static String startInputFlagsToString(int startInputFlags) {
final StringJoiner joiner = new StringJoiner("|");
if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) != 0) {
joiner.add("VIEW_HAS_FOCUS");
}
if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0) {
joiner.add("IS_TEXT_EDITOR");
}
if ((startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0) {
joiner.add("INITIAL_CONNECTION");
}
return joiner.setEmptyValue("(none)").toString();
}
}

View File

@@ -1,170 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.annotation.NonNull;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
import android.os.IBinder;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.Display;
/**
* A {@link Service} that implements multi-client IME protocol.
*/
public final class MultiClientInputMethod extends Service implements DisplayListener {
private static final String TAG = "MultiClientInputMethod";
private static final boolean DEBUG = false;
// last client that had active InputConnection for a given displayId.
final SparseIntArray mDisplayToLastClientId = new SparseIntArray();
// Mapping table from the display where IME is attached to the display where IME window will be
// shown. Assumes that missing display will use the same display for the IME window.
SparseIntArray mInputDisplayToImeDisplay;
SoftInputWindowManager mSoftInputWindowManager;
MultiClientInputMethodServiceDelegate mDelegate;
private DisplayManager mDisplayManager;
@Override
public void onCreate() {
if (DEBUG) {
Log.v(TAG, "onCreate");
}
mInputDisplayToImeDisplay = buildInputDisplayToImeDisplay();
mDelegate = MultiClientInputMethodServiceDelegate.create(this,
new MultiClientInputMethodServiceDelegate.ServiceCallback() {
@Override
public void initialized() {
if (DEBUG) {
Log.i(TAG, "initialized");
}
}
@Override
public void addClient(int clientId, int uid, int pid,
int selfReportedDisplayId) {
int imeDisplayId = mInputDisplayToImeDisplay.get(selfReportedDisplayId,
selfReportedDisplayId);
final ClientCallbackImpl callback = new ClientCallbackImpl(
MultiClientInputMethod.this, mDelegate,
mSoftInputWindowManager, clientId, uid, pid, imeDisplayId);
if (DEBUG) {
Log.v(TAG, "addClient clientId=" + clientId + " uid=" + uid
+ " pid=" + pid + " displayId=" + selfReportedDisplayId
+ " imeDisplayId=" + imeDisplayId);
}
mDelegate.acceptClient(clientId, callback, callback.getDispatcherState(),
callback.getLooper());
}
@Override
public void removeClient(int clientId) {
if (DEBUG) {
Log.v(TAG, "removeClient clientId=" + clientId);
}
}
});
mSoftInputWindowManager = new SoftInputWindowManager(this, mDelegate);
}
@Override
public void onDisplayAdded(int displayId) {
mInputDisplayToImeDisplay = buildInputDisplayToImeDisplay();
}
@Override
public void onDisplayRemoved(int displayId) {
mDisplayToLastClientId.delete(displayId);
}
@Override
public void onDisplayChanged(int displayId) {
}
@Override
public IBinder onBind(Intent intent) {
if (DEBUG) {
Log.v(TAG, "onBind intent=" + intent);
}
mDisplayManager = getApplicationContext().getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(this, getMainThreadHandler());
return mDelegate.onBind(intent);
}
@Override
public boolean onUnbind(Intent intent) {
if (DEBUG) {
Log.v(TAG, "onUnbind intent=" + intent);
}
if (mDisplayManager != null) {
mDisplayManager.unregisterDisplayListener(this);
}
return mDelegate.onUnbind(intent);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.v(TAG, "onDestroy");
}
mDelegate.onDestroy();
}
@NonNull
private SparseIntArray buildInputDisplayToImeDisplay() {
Context context = getApplicationContext();
String config[] = context.getResources().getStringArray(
R.array.config_inputDisplayToImeDisplay);
SparseIntArray inputDisplayToImeDisplay = new SparseIntArray();
Display[] displays = context.getSystemService(DisplayManager.class).getDisplays();
for (String item: config) {
String[] pair = item.split("/");
if (pair.length != 2) {
Log.w(TAG, "Skip illegal config: " + item);
continue;
}
int inputDisplay = findDisplayId(displays, pair[0]);
int imeDisplay = findDisplayId(displays, pair[1]);
if (inputDisplay != Display.INVALID_DISPLAY && imeDisplay != Display.INVALID_DISPLAY) {
inputDisplayToImeDisplay.put(inputDisplay, imeDisplay);
}
}
return inputDisplayToImeDisplay;
}
private static int findDisplayId(Display displays[], String regexp) {
for (Display display: displays) {
if (display.getUniqueId().matches(regexp)) {
int displayId = display.getDisplayId();
if (DEBUG) {
Log.v(TAG, regexp + " matches displayId=" + displayId);
}
return displayId;
}
}
Log.w(TAG, "Can't find the display of " + regexp);
return Display.INVALID_DISPLAY;
}
}

View File

@@ -1,56 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.inputmethodservice.KeyboardView;
/**
* Provides the no-op implementation of {@link KeyboardView.OnKeyboardActionListener}
*/
class NoopKeyboardActionListener implements KeyboardView.OnKeyboardActionListener {
@Override
public void onPress(int primaryCode) {
}
@Override
public void onRelease(int primaryCode) {
}
@Override
public void onKey(int primaryCode, int[] keyCodes) {
}
@Override
public void onText(CharSequence text) {
}
@Override
public void swipeLeft() {
}
@Override
public void swipeRight() {
}
@Override
public void swipeDown() {
}
@Override
public void swipeUp() {
}
}

View File

@@ -1,211 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.app.Dialog;
import android.content.Context;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.view.inputmethod.InputConnection;
import android.widget.LinearLayout;
import java.util.Arrays;
final class SoftInputWindow extends Dialog {
private static final String TAG = "SoftInputWindow";
private static final boolean DEBUG = false;
private final KeyboardView mKeyboardView;
private final Keyboard mQwertygKeyboard;
private final Keyboard mSymbolKeyboard;
private final Keyboard mSymbolShiftKeyboard;
private int mClientId = MultiClientInputMethodServiceDelegate.INVALID_CLIENT_ID;
private int mTargetWindowHandle = MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
private static final KeyboardView.OnKeyboardActionListener sNoopListener =
new NoopKeyboardActionListener();
SoftInputWindow(Context context, IBinder token) {
super(context, android.R.style.Theme_DeviceDefault_InputMethod);
final LayoutParams lp = getWindow().getAttributes();
lp.type = LayoutParams.TYPE_INPUT_METHOD;
lp.setTitle("InputMethod");
lp.gravity = Gravity.BOTTOM;
lp.width = LayoutParams.MATCH_PARENT;
lp.height = LayoutParams.WRAP_CONTENT;
lp.token = token;
getWindow().setAttributes(lp);
final int windowSetFlags = LayoutParams.FLAG_LAYOUT_IN_SCREEN
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
final int windowModFlags = LayoutParams.FLAG_LAYOUT_IN_SCREEN
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_DIM_BEHIND
| LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
getWindow().setFlags(windowSetFlags, windowModFlags);
final LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
mKeyboardView = (KeyboardView) getLayoutInflater().inflate(R.layout.input, null);
mQwertygKeyboard = new Keyboard(context, R.xml.qwerty);
mSymbolKeyboard = new Keyboard(context, R.xml.symbols);
mSymbolShiftKeyboard = new Keyboard(context, R.xml.symbols_shift);
mKeyboardView.setKeyboard(mQwertygKeyboard);
mKeyboardView.setOnKeyboardActionListener(sNoopListener);
// TODO(b/158663354): Disabling keyboard popped preview key since it is currently broken.
mKeyboardView.setPreviewEnabled(false);
layout.addView(mKeyboardView);
setContentView(layout, new ViewGroup.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// TODO: Check why we need to call this.
getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
int getClientId() {
return mClientId;
}
int getTargetWindowHandle() {
return mTargetWindowHandle;
}
boolean isQwertyKeyboard() {
return mKeyboardView.getKeyboard() == mQwertygKeyboard;
}
boolean isSymbolKeyboard() {
Keyboard keyboard = mKeyboardView.getKeyboard();
return keyboard == mSymbolKeyboard || keyboard == mSymbolShiftKeyboard;
}
void onFinishClient() {
mKeyboardView.setOnKeyboardActionListener(sNoopListener);
mClientId = MultiClientInputMethodServiceDelegate.INVALID_CLIENT_ID;
mTargetWindowHandle = MultiClientInputMethodServiceDelegate.INVALID_WINDOW_HANDLE;
}
void onDummyStartInput(int clientId, int targetWindowHandle) {
if (DEBUG) {
Log.v(TAG, "onDummyStartInput clientId=" + clientId
+ " targetWindowHandle=" + targetWindowHandle);
}
mKeyboardView.setOnKeyboardActionListener(sNoopListener);
mClientId = clientId;
mTargetWindowHandle = targetWindowHandle;
}
void onStartInput(int clientId, int targetWindowHandle, InputConnection inputConnection) {
if (DEBUG) {
Log.v(TAG, "onStartInput clientId=" + clientId
+ " targetWindowHandle=" + targetWindowHandle);
}
mClientId = clientId;
mTargetWindowHandle = targetWindowHandle;
mKeyboardView.setOnKeyboardActionListener(new NoopKeyboardActionListener() {
@Override
public void onKey(int primaryCode, int[] keyCodes) {
if (DEBUG) {
Log.v(TAG, "onKey clientId=" + clientId + " primaryCode=" + primaryCode
+ " keyCodes=" + Arrays.toString(keyCodes));
}
boolean isShifted = isShifted(); // Store the current state before resetting it.
resetShift();
switch (primaryCode) {
case Keyboard.KEYCODE_CANCEL:
hide();
break;
case Keyboard.KEYCODE_DELETE:
inputConnection.sendKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
inputConnection.sendKeyEvent(
new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
break;
case Keyboard.KEYCODE_MODE_CHANGE:
handleSwitchKeyboard();
break;
case Keyboard.KEYCODE_SHIFT:
handleShift(isShifted);
break;
default:
handleCharacter(inputConnection, primaryCode, isShifted);
break;
}
}
@Override
public void onText(CharSequence text) {
if (DEBUG) {
Log.v(TAG, "onText clientId=" + clientId + " text=" + text);
}
if (inputConnection == null) {
return;
}
inputConnection.commitText(text, 0);
}
});
}
void handleSwitchKeyboard() {
if (isQwertyKeyboard()) {
mKeyboardView.setKeyboard(mSymbolKeyboard);
} else {
mKeyboardView.setKeyboard(mQwertygKeyboard);
}
}
boolean isShifted() {
return mKeyboardView.isShifted();
}
void resetShift() {
if (isSymbolKeyboard() && isShifted()) {
mKeyboardView.setKeyboard(mSymbolKeyboard);
}
mKeyboardView.setShifted(false);
}
void handleShift(boolean isShifted) {
if (isSymbolKeyboard()) {
mKeyboardView.setKeyboard(isShifted ? mSymbolKeyboard : mSymbolShiftKeyboard);
}
mKeyboardView.setShifted(!isShifted);
}
void handleCharacter(InputConnection inputConnection, int primaryCode, boolean isShifted) {
if (isQwertyKeyboard() && isShifted) {
primaryCode = Character.toUpperCase(primaryCode);
}
inputConnection.commitText(String.valueOf((char) primaryCode), 1);
}
}

View File

@@ -1,61 +0,0 @@
/*
* 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.
*/
package com.example.android.multiclientinputmethod;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.inputmethodservice.MultiClientInputMethodServiceDelegate;
import android.os.IBinder;
import android.util.SparseArray;
import android.view.Display;
final class SoftInputWindowManager {
private final Context mContext;
private final MultiClientInputMethodServiceDelegate mDelegate;
private final SparseArray<SoftInputWindow> mSoftInputWindows = new SparseArray<>();
SoftInputWindowManager(Context context, MultiClientInputMethodServiceDelegate delegate) {
mContext = context;
mDelegate = delegate;
}
SoftInputWindow getOrCreateSoftInputWindow(int displayId) {
final SoftInputWindow existingWindow = mSoftInputWindows.get(displayId);
if (existingWindow != null) {
return existingWindow;
}
final Display display =
mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
if (display == null) {
return null;
}
final IBinder windowToken = mDelegate.createInputMethodWindowToken(displayId);
if (windowToken == null) {
return null;
}
final Context displayContext = mContext.createDisplayContext(display);
final SoftInputWindow newWindow = new SoftInputWindow(displayContext, windowToken);
mSoftInputWindows.put(displayId, newWindow);
return newWindow;
}
SoftInputWindow getSoftInputWindow(int displayId) {
return mSoftInputWindows.get(displayId);
}
}

View File

@@ -1,5 +1,5 @@
//
// Copyright (C) 2018 The Android Open Source Project
// 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.
@@ -15,17 +15,17 @@
//
package {
// See: http://go/android-license-faq
default_applicable_licenses: ["Android-Apache-2.0"],
}
android_app {
name: "MultiClientInputMethod",
srcs: ["**/*.java"],
platform_apis: true,
certificate: "platform",
privileged: true,
dex_preopt: {
enabled: false,
},
name: "SampleInputMethodAccessibilityService",
min_sdk_version: "33",
target_sdk_version: "33",
sdk_version: "current",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
static_libs: [
"androidx.annotation_annotation",
],
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.sampleinputmethodaccessibilityservice">
<application android:label="SampleInputMethodAccessibilityService">
<service android:name=".SampleInputMethodAccessibilityService"
android:exported="true"
android:label="SampleInputMethodAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
<category android:name="android.accessibilityservice.category.FEEDBACK_GENERIC" />
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<!--
~ 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.
-->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagInputMethodEditor"
android:notificationTimeout="0" />

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.view.MotionEvent;
import android.view.View;
final class DragToMoveTouchListener implements View.OnTouchListener {
@FunctionalInterface
interface OnMoveCallback {
void onMove(int dx, int dy);
}
private final OnMoveCallback mCallback;
private int mPointId = -1;
private float mLastTouchX;
private float mLastTouchY;
DragToMoveTouchListener(OnMoveCallback callback) {
mCallback = callback;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
final int pointId = event.getPointerId(event.getActionIndex());
switch (event.getAction()) {
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN: {
if (mPointId != -1) {
break;
}
v.setPressed(true);
mPointId = pointId;
mLastTouchX = event.getRawX();
mLastTouchY = event.getRawY();
break;
}
case MotionEvent.ACTION_MOVE: {
if (pointId != mPointId) {
break;
}
final float x = event.getRawX();
final float y = event.getRawY();
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
mCallback.onMove((int) dx, (int) dy);
mLastTouchX = x;
mLastTouchY = y;
break;
}
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (pointId != mPointId) {
break;
}
mPointId = -1;
v.setPressed(false);
break;
}
default:
break;
}
return true;
}
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
final class EditorInfoUtil {
/**
* Not intended to be instantiated.
*/
private EditorInfoUtil() {
}
static String dump(@Nullable EditorInfo editorInfo) {
if (editorInfo == null) {
return "null";
}
final StringBuilder sb = new StringBuilder();
dump(sb, editorInfo);
return sb.toString();
}
static void dump(@NonNull StringBuilder sb, @NonNull EditorInfo editorInfo) {
sb.append("packageName=").append(editorInfo.packageName).append("\n")
.append("inputType=");
dumpInputType(sb, editorInfo.inputType);
sb.append("\n");
sb.append("imeOptions=");
dumpImeOptions(sb, editorInfo.imeOptions);
sb.append("\n");
sb.append("initialSelection=(").append(editorInfo.initialSelStart)
.append(",").append(editorInfo.initialSelEnd).append(")");
sb.append("\n");
sb.append("initialCapsMode=");
dumpCapsMode(sb, editorInfo.initialCapsMode);
sb.append("\n");
}
static void dumpInputType(@NonNull StringBuilder sb, int inputType) {
final int inputClass = inputType & EditorInfo.TYPE_MASK_CLASS;
final int inputVariation = inputType & EditorInfo.TYPE_MASK_VARIATION;
final int inputFlags = inputType & EditorInfo.TYPE_MASK_FLAGS;
switch (inputClass) {
case EditorInfo.TYPE_NULL:
sb.append("Null");
break;
case EditorInfo.TYPE_CLASS_TEXT: {
sb.append("Text");
switch (inputVariation) {
case EditorInfo.TYPE_TEXT_VARIATION_NORMAL:
break;
case EditorInfo.TYPE_TEXT_VARIATION_URI:
sb.append(":URI");
break;
case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
sb.append(":EMAIL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT:
sb.append(":EMAIL_SUBJECT");
break;
case EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE:
sb.append(":SHORT_MESSAGE");
break;
case EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE:
sb.append(":LONG_MESSAGE");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME:
sb.append(":PERSON_NAME");
break;
case EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS:
sb.append(":POSTAL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PASSWORD:
sb.append(":PASSWORD");
break;
case EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
sb.append(":VISIBLE_PASSWORD");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT:
sb.append(":WEB_EDIT_TEXT");
break;
case EditorInfo.TYPE_TEXT_VARIATION_FILTER:
sb.append(":FILTER");
break;
case EditorInfo.TYPE_TEXT_VARIATION_PHONETIC:
sb.append(":PHONETIC");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
sb.append(":WEB_EMAIL_ADDRESS");
break;
case EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD:
sb.append(":WEB_PASSWORD");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
sb.append("|CAP_CHARACTERS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
sb.append("|CAP_WORDS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
sb.append("|CAP_SENTENCES");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0) {
sb.append("|AUTO_CORRECT");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
sb.append("|AUTO_COMPLETE");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0) {
sb.append("|MULTI_LINE");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
sb.append("|NO_SUGGESTIONS");
}
if ((inputFlags & EditorInfo.TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS)
!= 0) {
sb.append("|ENABLE_TEXT_CONVERSION_SUGGESTIONS");
}
break;
}
case EditorInfo.TYPE_CLASS_NUMBER: {
sb.append("Number");
switch (inputVariation) {
case EditorInfo.TYPE_NUMBER_VARIATION_NORMAL:
sb.append(":NORMAL");
break;
case EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD:
sb.append(":PASSWORD");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
if ((inputFlags & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0) {
sb.append("|SIGNED");
}
if ((inputFlags & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0) {
sb.append("|DECIMAL");
}
break;
}
case EditorInfo.TYPE_CLASS_PHONE:
sb.append("Phone");
break;
case EditorInfo.TYPE_CLASS_DATETIME: {
sb.append("DateTime");
switch (inputVariation) {
case EditorInfo.TYPE_DATETIME_VARIATION_NORMAL:
sb.append(":NORMAL");
break;
case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
sb.append(":DATE");
break;
case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
sb.append(":TIME");
break;
default:
sb.append(":UNKNOWN=").append(inputVariation);
break;
}
break;
}
default:
sb.append("UnknownClass=").append(inputClass);
if (inputVariation != 0) {
sb.append(":variation=").append(inputVariation);
}
if (inputFlags != 0) {
sb.append("|flags=0x").append(Integer.toHexString(inputFlags));
}
break;
}
}
static void dumpImeOptions(@NonNull StringBuilder sb, int imeOptions) {
final int action = imeOptions & EditorInfo.IME_MASK_ACTION;
final int flags = imeOptions & ~EditorInfo.IME_MASK_ACTION;
sb.append("Action:");
switch (action) {
case EditorInfo.IME_ACTION_UNSPECIFIED:
sb.append("UNSPECIFIED");
break;
case EditorInfo.IME_ACTION_NONE:
sb.append("NONE");
break;
case EditorInfo.IME_ACTION_GO:
sb.append("GO");
break;
case EditorInfo.IME_ACTION_SEARCH:
sb.append("SEARCH");
break;
case EditorInfo.IME_ACTION_SEND:
sb.append("SEND");
break;
case EditorInfo.IME_ACTION_NEXT:
sb.append("NEXT");
break;
case EditorInfo.IME_ACTION_DONE:
sb.append("DONE");
break;
case EditorInfo.IME_ACTION_PREVIOUS:
sb.append("PREVIOUS");
break;
default:
sb.append("UNKNOWN=").append(action);
break;
}
if ((flags & EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING) != 0) {
sb.append("|NO_PERSONALIZED_LEARNING");
}
if ((flags & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0) {
sb.append("|NO_FULLSCREEN");
}
if ((flags & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0) {
sb.append("|NAVIGATE_PREVIOUS");
}
if ((flags & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
sb.append("|NAVIGATE_NEXT");
}
if ((flags & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0) {
sb.append("|NO_EXTRACT_UI");
}
if ((flags & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) != 0) {
sb.append("|NO_ACCESSORY_ACTION");
}
if ((flags & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
sb.append("|NO_ENTER_ACTION");
}
if ((flags & EditorInfo.IME_FLAG_FORCE_ASCII) != 0) {
sb.append("|FORCE_ASCII");
}
}
static void dumpCapsMode(@NonNull StringBuilder sb, int capsMode) {
if (capsMode == 0) {
sb.append("none");
return;
}
boolean addSeparator = false;
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
sb.append("CHARACTERS");
addSeparator = true;
}
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
if (addSeparator) {
sb.append('|');
}
sb.append("WORDS");
addSeparator = true;
}
if ((capsMode & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
if (addSeparator) {
sb.append('|');
}
sb.append("SENTENCES");
}
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.os.Parcel;
import android.view.inputmethod.EditorInfo;
import androidx.annotation.Nullable;
final class EventMonitor {
@FunctionalInterface
interface DebugMessageCallback {
void onMessageChanged(String message);
}
private enum State {
BeforeFirstStartInput,
InputStarted,
InputRestarted,
InputFinished,
}
private State mState = State.BeforeFirstStartInput;
private int mStartInputCount = 0;
private int mUpdateSelectionCount = 0;
private int mFinishInputCount = 0;
private int mSelStart = -1;
private int mSelEnd = -1;
private int mCompositionStart = -1;
private int mCompositionEnd = -1;
@Nullable
private EditorInfo mEditorInfo;
@Nullable
private final DebugMessageCallback mDebugMessageCallback;
void onStartInput(EditorInfo attribute, boolean restarting) {
++mStartInputCount;
mState = restarting ? State.InputRestarted : State.InputStarted;
mSelStart = attribute.initialSelStart;
mSelEnd = attribute.initialSelEnd;
mCompositionStart = -1;
mCompositionEnd = -1;
mEditorInfo = cloneEditorInfo(attribute);
updateMessage();
}
void onFinishInput() {
++mFinishInputCount;
mState = State.InputFinished;
mEditorInfo = null;
updateMessage();
}
void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
++mUpdateSelectionCount;
mSelStart = newSelStart;
mSelEnd = newSelEnd;
mCompositionStart = candidatesStart;
mCompositionEnd = candidatesEnd;
updateMessage();
}
EventMonitor(@Nullable DebugMessageCallback callback) {
mDebugMessageCallback = callback;
}
private void updateMessage() {
if (mDebugMessageCallback == null) {
return;
}
final StringBuilder sb = new StringBuilder();
sb.append("state=").append(mState).append("\n")
.append("startInputCount=").append(mStartInputCount).append("\n")
.append("finishInputCount=").append(mFinishInputCount).append("\n")
.append("updateSelectionCount=").append(mUpdateSelectionCount).append("\n");
if (mSelStart == -1 && mSelEnd == -1) {
sb.append("selection=none\n");
} else {
sb.append("selection=(").append(mSelStart).append(",").append(mSelEnd).append(")\n");
}
if (mCompositionStart == -1 && mCompositionEnd == -1) {
sb.append("composition=none");
} else {
sb.append("composition=(")
.append(mCompositionStart).append(",").append(mCompositionEnd).append(")");
}
if (mEditorInfo != null) {
sb.append("\n");
sb.append("packageName=").append(mEditorInfo.packageName).append("\n");
sb.append("inputType=");
EditorInfoUtil.dumpInputType(sb, mEditorInfo.inputType);
sb.append("\n");
sb.append("imeOptions=");
EditorInfoUtil.dumpImeOptions(sb, mEditorInfo.imeOptions);
}
mDebugMessageCallback.onMessageChanged(sb.toString());
}
private static EditorInfo cloneEditorInfo(EditorInfo original) {
Parcel parcel = null;
try {
parcel = Parcel.obtain();
original.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
return EditorInfo.CREATOR.createFromParcel(parcel);
} finally {
if (parcel != null) {
parcel.recycle();
}
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
final class OverlayWindowBuilder {
@NonNull
private final View mContentView;
private int mWidth = WindowManager.LayoutParams.WRAP_CONTENT;
private int mHeight = WindowManager.LayoutParams.WRAP_CONTENT;
private int mGravity = Gravity.NO_GRAVITY;
private int mRelX = 0;
private int mRelY = 0;
private Integer mBackgroundColor = null;
private boolean mShown = false;
private OverlayWindowBuilder(@NonNull View contentView) {
mContentView = contentView;
}
static OverlayWindowBuilder from(@NonNull View contentView) {
return new OverlayWindowBuilder(contentView);
}
OverlayWindowBuilder setSize(int width, int height) {
mWidth = width;
mHeight = height;
return this;
}
OverlayWindowBuilder setGravity(int gravity) {
mGravity = gravity;
return this;
}
OverlayWindowBuilder setRelativePosition(int relX, int relY) {
mRelX = relX;
mRelY = relY;
return this;
}
OverlayWindowBuilder setBackgroundColor(@ColorInt int color) {
mBackgroundColor = color;
return this;
}
void show() {
if (mShown) {
throw new UnsupportedOperationException("show() can be called only once.");
}
final Context context = mContentView.getContext();
final WindowManager windowManager = context.getSystemService(WindowManager.class);
final FrameLayout contentFrame = new FrameLayout(context) {
@Override
public boolean requestSendAccessibilityEvent(View view, AccessibilityEvent event) {
return false;
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
}
};
if (mBackgroundColor != null) {
contentFrame.setBackgroundColor(mBackgroundColor);
}
contentFrame.setOnTouchListener(new DragToMoveTouchListener((dx, dy) -> {
final WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) contentFrame.getLayoutParams();
lp.x += dx;
lp.y += dy;
windowManager.updateViewLayout(contentFrame, lp);
}));
contentFrame.addView(mContentView);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
mWidth, mHeight,
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.gravity = mGravity;
params.x = mRelX;
params.y = mRelY;
windowManager.addView(contentFrame, params);
mShown = true;
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.sampleinputmethodaccessibilityservice;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.InputMethod;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import android.view.Gravity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import java.util.function.BiConsumer;
/**
* A sample {@link AccessibilityService} to demo how to use IME APIs.
*/
public final class SampleInputMethodAccessibilityService extends AccessibilityService {
private static final String TAG = "SampleImeA11yService";
private EventMonitor mEventMonitor;
private final class InputMethodImpl extends InputMethod {
InputMethodImpl(AccessibilityService service) {
super(service);
}
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
Log.d(TAG, String.format("onStartInput(%s,%b)", attribute, restarting));
mEventMonitor.onStartInput(attribute, restarting);
}
@Override
public void onFinishInput() {
Log.d(TAG, "onFinishInput()");
mEventMonitor.onFinishInput();
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart,
int newSelEnd, int candidatesStart, int candidatesEnd) {
Log.d(TAG, String.format("onUpdateSelection(%d,%d,%d,%d,%d,%d)", oldSelStart, oldSelEnd,
newSelStart, newSelEnd, candidatesStart, candidatesEnd));
mEventMonitor.onUpdateSelection(oldSelStart, oldSelEnd,
newSelStart, newSelEnd, candidatesStart, candidatesEnd);
}
}
private static <T> Pair<CharSequence, T> item(@NonNull CharSequence label, @Nullable T value) {
return Pair.create(label, value);
}
private <T> void addButtons(@NonNull LinearLayout parentView, @NonNull String headerText,
@NonNull List<Pair<CharSequence, T>> items,
@NonNull BiConsumer<T, InputMethod.AccessibilityInputConnection> action) {
final LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
{
final TextView headerTextView = new TextView(this, null,
android.R.attr.listSeparatorTextViewStyle);
headerTextView.setAllCaps(false);
headerTextView.setText(headerText);
layout.addView(headerTextView);
}
{
final LinearLayout itemLayout = new LinearLayout(this);
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
for (Pair<CharSequence, T> item : items) {
final Button button = new Button(this, null, android.R.attr.buttonStyleSmall);
button.setAllCaps(false);
button.setText(item.first);
button.setOnClickListener(view -> {
final InputMethod ime = getInputMethod();
if (ime == null) {
return;
}
final InputMethod.AccessibilityInputConnection ic =
ime.getCurrentInputConnection();
if (ic == null) {
return;
}
action.accept(item.second, ic);
});
itemLayout.addView(button);
}
final HorizontalScrollView scrollView = new HorizontalScrollView(this);
scrollView.addView(itemLayout);
layout.addView(scrollView);
}
parentView.addView(layout);
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
final WindowManager windowManager = getSystemService(WindowManager.class);
final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
// Create a monitor window.
{
final TextView textView = new TextView(this);
mEventMonitor = new EventMonitor(textView::setText);
final LinearLayout monitorWindowContent = new LinearLayout(this);
monitorWindowContent.setOrientation(LinearLayout.VERTICAL);
monitorWindowContent.setPadding(10, 10, 10, 10);
monitorWindowContent.addView(textView);
OverlayWindowBuilder.from(monitorWindowContent)
.setSize((metrics.getBounds().width() * 3) / 4,
WindowManager.LayoutParams.WRAP_CONTENT)
.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
.setBackgroundColor(0xeed2e3fc)
.show();
}
final LinearLayout contentView = new LinearLayout(this);
contentView.setOrientation(LinearLayout.VERTICAL);
{
final TextView textView = new TextView(this, null, android.R.attr.windowTitleStyle);
textView.setGravity(Gravity.CENTER);
textView.setText("A11Y IME");
contentView.addView(textView);
}
{
final LinearLayout buttonLayout = new LinearLayout(this);
buttonLayout.setBackgroundColor(0xfffeefc3);
buttonLayout.setPadding(10, 10, 10, 10);
buttonLayout.setOrientation(LinearLayout.VERTICAL);
addButtons(buttonLayout,
"commitText", List.of(
item("A", "A"),
item("Hello World", "Hello World"),
item("\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F",
"\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F")),
(value, ic) -> ic.commitText(value, 1, null));
addButtons(buttonLayout,
"sendKeyEvent", List.of(
item("A", KeyEvent.KEYCODE_A),
item("DEL", KeyEvent.KEYCODE_DEL),
item("DPAD_LEFT", KeyEvent.KEYCODE_DPAD_LEFT),
item("DPAD_RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT),
item("COPY", KeyEvent.KEYCODE_COPY),
item("CUT", KeyEvent.KEYCODE_CUT),
item("PASTE", KeyEvent.KEYCODE_PASTE)),
(keyCode, ic) -> {
final long eventTime = SystemClock.uptimeMillis();
ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
ic.sendKeyEvent(new KeyEvent(eventTime, SystemClock.uptimeMillis(),
KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
});
addButtons(buttonLayout,
"performEditorAction", List.of(
item("UNSPECIFIED", EditorInfo.IME_ACTION_UNSPECIFIED),
item("NONE", EditorInfo.IME_ACTION_NONE),
item("GO", EditorInfo.IME_ACTION_GO),
item("SEARCH", EditorInfo.IME_ACTION_SEARCH),
item("SEND", EditorInfo.IME_ACTION_SEND),
item("NEXT", EditorInfo.IME_ACTION_NEXT),
item("DONE", EditorInfo.IME_ACTION_DONE),
item("PREVIOUS", EditorInfo.IME_ACTION_PREVIOUS)),
(action, ic) -> ic.performEditorAction(action));
addButtons(buttonLayout,
"performContextMenuAction", List.of(
item("selectAll", android.R.id.selectAll),
item("startSelectingText", android.R.id.startSelectingText),
item("stopSelectingText", android.R.id.stopSelectingText),
item("cut", android.R.id.cut),
item("copy", android.R.id.copy),
item("paste", android.R.id.paste),
item("copyUrl", android.R.id.copyUrl),
item("switchInputMethod", android.R.id.switchInputMethod)),
(action, ic) -> ic.performContextMenuAction(action));
addButtons(buttonLayout,
"setSelection", List.of(
item("(0,0)", Pair.create(0, 0)),
item("(0,1)", Pair.create(0, 1)),
item("(1,1)", Pair.create(1, 1)),
item("(0,999)", Pair.create(0, 999))),
(pair, ic) -> ic.setSelection(pair.first, pair.second));
addButtons(buttonLayout,
"deleteSurroundingText", List.of(
item("(0,0)", Pair.create(0, 0)),
item("(0,1)", Pair.create(0, 1)),
item("(1,0)", Pair.create(1, 0)),
item("(1,1)", Pair.create(1, 1)),
item("(999,0)", Pair.create(999, 0)),
item("(0,999)", Pair.create(0, 999))),
(pair, ic) -> ic.deleteSurroundingText(pair.first, pair.second));
final ScrollView scrollView = new ScrollView(this);
scrollView.addView(buttonLayout);
contentView.addView(scrollView);
// Set margin
{
final LinearLayout.LayoutParams lp =
((LinearLayout.LayoutParams) scrollView.getLayoutParams());
lp.leftMargin = lp.rightMargin = lp.bottomMargin = 20;
scrollView.setLayoutParams(lp);
}
}
OverlayWindowBuilder.from(contentView)
.setSize((metrics.getBounds().width() * 3) / 4,
metrics.getBounds().height() / 5)
.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
.setRelativePosition(300, 300)
.setBackgroundColor(0xfffcc934)
.show();
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
}
@Override
public void onInterrupt() {
}
@Override
public InputMethod onCreateInputMethod() {
Log.d(TAG, "onCreateInputMethod");
return new InputMethodImpl(this);
}
}

View File

@@ -21,6 +21,8 @@ package {
android_app {
name: "ThemedNavBarKeyboard",
srcs: ["**/*.java"],
min_sdk_version: "28",
target_sdk_version: "31",
sdk_version: "current",
dex_preopt: {
enabled: false,

View File

@@ -19,6 +19,7 @@
<application android:label="ThemedNavBarKeyboard">
<service android:name=".ThemedNavBarKeyboard"
android:exported="true"
android:label="ThemedNavBarKeyboard"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>

View File

@@ -59,7 +59,7 @@ public class ThemedNavBarKeyboard extends InputMethodService {
@Override
public void onCreate() {
super.onCreate();
if (BuildCompat.EFFECTIVE_SDK_INT > Build.VERSION_CODES.P) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Disable contrast for extended navbar gradient.
getWindow().getWindow().setNavigationBarContrastEnforced(false);
}
@@ -189,6 +189,29 @@ public class ThemedNavBarKeyboard extends InputMethodService {
switchToSeparateNavBarMode(Color.DKGRAY, false /* lightNavBar */);
setBackgroundColor(MINT_COLOR);
{
final LinearLayout subLayout = new LinearLayout(context);
{
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
lp.weight = 50;
subLayout.addView(createButton("BACK_DISPOSITION\nDEFAULT", () -> {
setBackDisposition(BACK_DISPOSITION_DEFAULT);
}), lp);
}
{
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
lp.weight = 50;
subLayout.addView(createButton("BACK_DISPOSITION\nADJUST_NOTHING", () -> {
setBackDisposition(BACK_DISPOSITION_ADJUST_NOTHING);
}), lp);
}
addView(subLayout);
}
addView(createButton("Floating Mode", () -> {
switchToFloatingMode();
setBackgroundColor(Color.TRANSPARENT);

View File

@@ -1,3 +1,3 @@
Pkg.UserSrc=false
Pkg.Revision=${PLATFORM_SDK_VERSION}.0.0
#Pkg.Revision=32.0.0 rc2
#Pkg.Revision=33.0.0 rc4

View File

@@ -2,7 +2,7 @@ Pkg.Desc=Android SDK Platform ${PLATFORM_VERSION}
Pkg.UserSrc=false
Platform.Version=${PLATFORM_VERSION}
Platform.CodeName=
Pkg.Revision=1
Pkg.Revision=2
AndroidVersion.ApiLevel=${PLATFORM_SDK_VERSION}
AndroidVersion.CodeName=${PLATFORM_VERSION_CODENAME}
AndroidVersion.ExtensionLevel=${PLATFORM_SDK_EXTENSION_VERSION}

View File

@@ -47,7 +47,7 @@ LOG_LEVEL = logging.DEBUG
PORT = 5544
# Keep in sync with WINSCOPE_PROXY_VERSION in Winscope DataAdb.vue
VERSION = '0.5'
VERSION = '0.8'
WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
WINSCOPE_TOKEN_HEADER = "Winscope-Token"
@@ -55,6 +55,14 @@ WINSCOPE_TOKEN_HEADER = "Winscope-Token"
# Location to save the proxy security token
WINSCOPE_TOKEN_LOCATION = os.path.expanduser('~/.config/winscope/.token')
# Winscope traces extensions
WINSCOPE_EXT = ".winscope"
WINSCOPE_EXT_LEGACY = ".pb"
WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]
# Winscope traces directory
WINSCOPE_DIR = "/data/misc/wmtrace/"
# Max interval between the client keep-alive requests in seconds
KEEP_ALIVE_INTERVAL_S = 5
@@ -63,50 +71,213 @@ logging.basicConfig(stream=sys.stderr, level=LOG_LEVEL,
log = logging.getLogger("ADBProxy")
class File:
def __init__(self, file, filetype) -> None:
self.file = file
self.type = filetype
def get_filepaths(self, device_id):
return [self.file]
def get_filetype(self):
return self.type
class FileMatcher:
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.matcher = matcher
self.type = filetype
def get_filepaths(self, device_id):
matchingFiles = call_adb(
f"shell su root find {self.path} -name {self.matcher}", device_id)
log.debug("Found file %s", matchingFiles.split('\n')[:-1])
return matchingFiles.split('\n')[:-1]
def get_filetype(self):
return self.type
class WinscopeFileMatcher(FileMatcher):
def __init__(self, path, matcher, filetype) -> None:
self.path = path
self.internal_matchers = list(map(lambda ext: FileMatcher(path, f'{matcher}{ext}', filetype),
WINSCOPE_EXTS))
self.type = filetype
def get_filepaths(self, device_id):
for matcher in self.internal_matchers:
files = matcher.get_filepaths(device_id)
if len(files) > 0:
return files
log.debug("No files found")
return []
class TraceTarget:
"""Defines a single parameter to trace.
Attributes:
file: the path on the device the trace results are saved to.
file_matchers: the matchers used to identify the paths on the device the trace results are saved to.
trace_start: command to start the trace from adb shell, must not block.
trace_stop: command to stop the trace, should block until the trace is stopped.
"""
def __init__(self, file: str, trace_start: str, trace_stop: str) -> None:
self.file = file
def __init__(self, files, trace_start: str, trace_stop: str) -> None:
if type(files) is not list:
files = [files]
self.files = files
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 = {
"window_trace": TraceTarget(
"/data/misc/wmtrace/wm_trace.pb",
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(
"/data/misc/wmtrace/layers_trace.pb",
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'
),
"screen_recording": TraceTarget(
"/data/local/tmp/screen.winscope.mp4",
'screenrecord --bit-rate 8M /data/local/tmp/screen.winscope.mp4 >/dev/null 2>&1 &\necho "ScreenRecorder started."',
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."',
'pkill -l SIGINT screenrecord >/dev/null 2>&1'
),
"transaction": TraceTarget(
"/data/misc/wmtrace/transaction_trace.pb",
"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'
),
"transactions_legacy": TraceTarget(
[
WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
],
'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1'
),
"proto_log": TraceTarget(
"/data/misc/wmtrace/wm_log.pb",
WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
'su root cmd window logging start\necho "WM logging started."',
'su root cmd window logging stop >/dev/null 2>&1'
),
"ime_trace_clients": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
'su root ime tracing start\necho "Clients IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_service": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
'su root ime tracing start\necho "Service IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"ime_trace_managerservice": TraceTarget(
WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice"),
'su root ime tracing start\necho "ManagerService IME trace started."',
'su root ime tracing stop >/dev/null 2>&1'
),
"wayland_trace": TraceTarget(
WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
'su root service call Wayland 26 i32 0 >/dev/null'
),
}
class SurfaceFlingerTraceConfig:
"""Handles optional configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# default config flags CRITICAL | INPUT | SYNC
self.flags = 1 << 0 | 1 << 1 | 1 << 6
def add(self, config: str) -> None:
self.flags |= CONFIG_FLAG[config]
def is_valid(self, config: str) -> bool:
return config in CONFIG_FLAG
def command(self) -> str:
return f'su root service call SurfaceFlinger 1033 i32 {self.flags}'
class SurfaceFlingerTraceSelectedConfig:
"""Handles optional selected configuration for surfaceflinger traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"sfbuffersize": "16000"
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_SF_SELECTION
def setBufferSize(self) -> str:
return f'su root service call SurfaceFlinger 1029 i32 {self.selectedConfigs["sfbuffersize"]}'
class WindowManagerTraceSelectedConfig:
"""Handles optional selected configuration for windowmanager traces.
"""
def __init__(self) -> None:
# defaults set for all configs
self.selectedConfigs = {
"wmbuffersize": "16000",
"tracinglevel": "debug",
"tracingtype": "frame",
}
def add(self, configType, configValue) -> None:
self.selectedConfigs[configType] = configValue
def is_valid(self, configType) -> bool:
return configType in CONFIG_WM_SELECTION
def setBufferSize(self) -> str:
return f'su root cmd window tracing size {self.selectedConfigs["wmbuffersize"]}'
def setTracingLevel(self) -> str:
return f'su root cmd window tracing level {self.selectedConfigs["tracinglevel"]}'
def setTracingType(self) -> str:
return f'su root cmd window tracing {self.selectedConfigs["tracingtype"]}'
CONFIG_FLAG = {
"composition": 1 << 2,
"metadata": 1 << 3,
"hwc": 1 << 4,
"tracebuffers": 1 << 5
}
#Keep up to date with options in DataAdb.vue
CONFIG_SF_SELECTION = [
"sfbuffersize",
]
#Keep up to date with options in DataAdb.vue
CONFIG_WM_SELECTION = [
"wmbuffersize",
"tracingtype",
"tracinglevel",
]
class DumpTarget:
"""Defines a single parameter to trace.
@@ -115,19 +286,21 @@ class DumpTarget:
dump_command: command to dump state to file.
"""
def __init__(self, file: str, dump_command: str) -> None:
self.file = file
def __init__(self, files, dump_command: str) -> None:
if type(files) is not list:
files = [files]
self.files = files
self.dump_command = dump_command
DUMP_TARGETS = {
"window_dump": DumpTarget(
"/data/local/tmp/wm_dump.pb",
'su root dumpsys window --proto > /data/local/tmp/wm_dump.pb'
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(
"/data/local/tmp/sf_dump.pb",
'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump.pb'
File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
f'su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}'
)
}
@@ -140,18 +313,21 @@ def get_token() -> str:
try:
with open(WINSCOPE_TOKEN_LOCATION, 'r') as token_file:
token = token_file.readline()
log.debug("Loaded token {} from {}".format(token, WINSCOPE_TOKEN_LOCATION))
log.debug("Loaded token {} from {}".format(
token, WINSCOPE_TOKEN_LOCATION))
return token
except IOError:
token = secrets.token_hex(32)
os.makedirs(os.path.dirname(WINSCOPE_TOKEN_LOCATION), exist_ok=True)
try:
with open(WINSCOPE_TOKEN_LOCATION, 'w') as token_file:
log.debug("Created and saved token {} to {}".format(token, WINSCOPE_TOKEN_LOCATION))
log.debug("Created and saved token {} to {}".format(
token, WINSCOPE_TOKEN_LOCATION))
token_file.write(token)
os.chmod(WINSCOPE_TOKEN_LOCATION, 0o600)
except IOError:
log.error("Unable to save persistent token {} to {}".format(token, WINSCOPE_TOKEN_LOCATION))
log.error("Unable to save persistent token {} to {}".format(
token, WINSCOPE_TOKEN_LOCATION))
return token
@@ -168,8 +344,10 @@ def add_standard_headers(server):
server.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
server.send_header('Access-Control-Allow-Origin', '*')
server.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
server.send_header('Access-Control-Allow-Headers', WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
server.send_header('Access-Control-Expose-Headers', 'Winscope-Proxy-Version')
server.send_header('Access-Control-Allow-Headers',
WINSCOPE_TOKEN_HEADER + ', Content-Type, Content-Length')
server.send_header('Access-Control-Expose-Headers',
'Winscope-Proxy-Version')
server.send_header(WINSCOPE_VERSION_HEADER, VERSION)
server.end_headers()
@@ -193,7 +371,7 @@ class BadRequest(Exception):
class RequestRouter:
"""Handles HTTP request authenticationn and routing"""
"""Handles HTTP request authentication and routing"""
def __init__(self, handler):
self.request = handler
@@ -209,7 +387,8 @@ class RequestRouter:
def __internal_error(self, error: str):
log.error("Internal error: " + error)
self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR, error.encode("utf-8"), 'text/txt')
self.request.respond(HTTPStatus.INTERNAL_SERVER_ERROR,
error.encode("utf-8"), 'text/txt')
def __bad_token(self):
log.info("Bad token")
@@ -242,11 +421,15 @@ def call_adb(params: str, device: str = None, stdin: bytes = None):
log.debug("Call: " + ' '.join(command))
return subprocess.check_output(command, stderr=subprocess.STDOUT, input=stdin).decode('utf-8')
except OSError as ex:
log.debug('Error executing adb command: {}\n{}'.format(' '.join(command), repr(ex)))
raise AdbError('Error executing adb command: {}\n{}'.format(' '.join(command), repr(ex)))
log.debug('Error executing adb command: {}\n{}'.format(
' '.join(command), repr(ex)))
raise AdbError('Error executing adb command: {}\n{}'.format(
' '.join(command), repr(ex)))
except subprocess.CalledProcessError as ex:
log.debug('Error executing adb command: {}\n{}'.format(' '.join(command), ex.output.decode("utf-8")))
raise AdbError('Error executing adb command: adb {}\n{}'.format(params, ex.output.decode("utf-8")))
log.debug('Error executing adb command: {}\n{}'.format(
' '.join(command), ex.output.decode("utf-8")))
raise AdbError('Error executing adb command: adb {}\n{}'.format(
params, ex.output.decode("utf-8")))
def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = None):
@@ -261,12 +444,33 @@ def call_adb_outfile(params: str, outfile, device: str = None, stdin: bytes = No
raise AdbError('Error executing adb command: adb {}\n'.format(params) + err.decode(
'utf-8') + '\n' + outfile.read().decode('utf-8'))
except OSError as ex:
log.debug('Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
raise AdbError('Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
log.debug('Error executing adb command: adb {}\n{}'.format(
params, repr(ex)))
raise AdbError(
'Error executing adb command: adb {}\n{}'.format(params, repr(ex)))
class CheckWaylandServiceEndpoint(RequestEndpoint):
_listDevicesEndpoint = None
def __init__(self, listDevicesEndpoint):
self._listDevicesEndpoint = listDevicesEndpoint
def process(self, server, path):
self._listDevicesEndpoint.process(server, path)
foundDevices = self._listDevicesEndpoint._foundDevices
if len(foundDevices) > 1:
res = 'false'
else:
raw_res = call_adb('shell service check Wayland')
res = 'false' if 'not found' in raw_res else 'true'
server.respond(HTTPStatus.OK, res.encode("utf-8"), "text/json")
class ListDevicesEndpoint(RequestEndpoint):
ADB_INFO_RE = re.compile("^([A-Za-z0-9.:\\-]+)\\s+(\\w+)(.*model:(\\w+))?")
_foundDevices = None
def process(self, server, path):
lines = list(filter(None, call_adb('devices -l').split('\n')))
@@ -274,6 +478,7 @@ class ListDevicesEndpoint(RequestEndpoint):
'authorised': str(m.group(2)) != 'unauthorized',
'model': m.group(4).replace('_', ' ') if m.group(4) else ''
} for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m}
self._foundDevices = devices
j = json.dumps(devices)
log.debug("Detected devices: " + j)
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
@@ -290,34 +495,54 @@ class DeviceRequestEndpoint(RequestEndpoint):
def process_with_device(self, server, path, device_id):
pass
def get_request(self, server) -> str:
try:
length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
return json.loads(server.rfile.read(length).decode("utf-8"))
class FetchFileEndpoint(DeviceRequestEndpoint):
class FetchFilesEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
if len(path) != 1:
raise BadRequest("File not specified")
if path[0] in TRACE_TARGETS:
file_path = TRACE_TARGETS[path[0]].file
files = TRACE_TARGETS[path[0]].files
elif path[0] in DUMP_TARGETS:
file_path = DUMP_TARGETS[path[0]].file
files = DUMP_TARGETS[path[0]].files
else:
raise BadRequest("Unknown file specified")
with NamedTemporaryFile() as tmp:
log.debug("Fetching file {} from device to {}".format(file_path, tmp.name))
call_adb_outfile('exec-out su root cat ' + file_path, tmp, device_id)
log.debug("Deleting file {} from device".format(file_path))
call_adb('shell su root rm ' + file_path, device_id)
server.send_response(HTTPStatus.OK)
server.send_header('X-Content-Type-Options', 'nosniff')
server.send_header('Content-type', 'application/octet-stream')
add_standard_headers(server)
log.debug("Uploading file {}".format(tmp.name))
while True:
buf = tmp.read(1024)
if buf:
server.wfile.write(buf)
else:
break
file_buffers = dict()
for f in files:
file_type = f.get_filetype()
file_paths = f.get_filepaths(device_id)
for file_path in file_paths:
with NamedTemporaryFile() as tmp:
log.debug(
f"Fetching file {file_path} from device to {tmp.name}")
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)
log.debug(f"Uploading file {tmp.name}")
if file_type not in file_buffers:
file_buffers[file_type] = []
buf = base64.encodebytes(tmp.read()).decode("utf-8")
file_buffers[file_type].append(buf)
if (len(file_buffers) == 0):
log.error("Proxy didn't find any file to fetch")
# server.send_header('X-Content-Type-Options', 'nosniff')
# add_standard_headers(server)
j = json.dumps(file_buffers)
server.respond(HTTPStatus.OK, j.encode("utf-8"), "text/json")
def check_root(device_id):
@@ -342,34 +567,41 @@ class TraceThread(threading.Thread):
self.process = subprocess.Popen(shell, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.PIPE, start_new_session=True)
except OSError as ex:
raise AdbError('Error executing adb command: adb shell\n{}'.format(repr(ex)))
raise AdbError(
'Error executing adb command: adb shell\n{}'.format(repr(ex)))
super().__init__()
def timeout(self):
if self.is_alive():
log.warning("Keep-alive timeout for trace on {}".format(self._device_id))
log.warning(
"Keep-alive timeout for trace on {}".format(self._device_id))
self.end_trace()
if self._device_id in TRACE_THREADS:
TRACE_THREADS.pop(self._device_id)
def reset_timer(self):
log.debug("Resetting keep-alive clock for trace on {}".format(self._device_id))
log.debug(
"Resetting keep-alive clock for trace on {}".format(self._device_id))
if self._keep_alive_timer:
self._keep_alive_timer.cancel()
self._keep_alive_timer = threading.Timer(KEEP_ALIVE_INTERVAL_S, self.timeout)
self._keep_alive_timer = threading.Timer(
KEEP_ALIVE_INTERVAL_S, self.timeout)
self._keep_alive_timer.start()
def end_trace(self):
if self._keep_alive_timer:
self._keep_alive_timer.cancel()
log.debug("Sending SIGINT to the trace process on {}".format(self._device_id))
log.debug("Sending SIGINT to the trace process on {}".format(
self._device_id))
self.process.send_signal(signal.SIGINT)
try:
log.debug("Waiting for trace shell to exit for {}".format(self._device_id))
log.debug("Waiting for trace shell to exit for {}".format(
self._device_id))
self.process.wait(timeout=5)
except TimeoutError:
log.debug("TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
log.debug(
"TIMEOUT - sending SIGKILL to the trace process on {}".format(self._device_id))
self.process.kill()
self.join()
@@ -379,10 +611,12 @@ class TraceThread(threading.Thread):
self.out, self.err = self.process.communicate(self.trace_command)
log.debug("Trace ended on {}, waiting for cleanup".format(self._device_id))
time.sleep(0.2)
for i in range(10):
for i in range(50):
if call_adb("shell su root cat /data/local/tmp/winscope_status", device=self._device_id) == 'TRACE_OK\n':
call_adb("shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
log.debug("Trace finished successfully on {}".format(self._device_id))
call_adb(
"shell su root rm /data/local/tmp/winscope_status", device=self._device_id)
log.debug("Trace finished successfully on {}".format(
self._device_id))
self._success = True
break
log.debug("Still waiting for cleanup on {}".format(self._device_id))
@@ -420,13 +654,7 @@ while true; do sleep 0.1; done
def process_with_device(self, server, path, device_id):
try:
length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
try:
requested_types = json.loads(server.rfile.read(length).decode("utf-8"))
requested_types = self.get_request(server)
requested_traces = [TRACE_TARGETS[t] for t in requested_types]
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
@@ -440,8 +668,10 @@ while true; do sleep 0.1; done
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]))
log.debug("Trace requested for {} with targets {}".format(device_id, ','.join(requested_types)))
TRACE_THREADS[device_id] = TraceThread(device_id, command.encode('utf-8'))
log.debug("Trace requested for {} with targets {}".format(
device_id, ','.join(requested_types)))
TRACE_THREADS[device_id] = TraceThread(
device_id, command.encode('utf-8'))
TRACE_THREADS[device_id].start()
server.respond(HTTPStatus.OK, b'', "text/plain")
@@ -454,7 +684,8 @@ class EndTrace(DeviceRequestEndpoint):
TRACE_THREADS[device_id].end_trace()
success = TRACE_THREADS[device_id].success()
out = TRACE_THREADS[device_id].out + b"\n" + TRACE_THREADS[device_id].err
out = TRACE_THREADS[device_id].out + \
b"\n" + TRACE_THREADS[device_id].err
command = TRACE_THREADS[device_id].trace_command
TRACE_THREADS.pop(device_id)
if success:
@@ -466,24 +697,97 @@ class EndTrace(DeviceRequestEndpoint):
"utf-8"))
def execute_command(server, device_id, shell, configType, configValue):
process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE, start_new_session=True)
log.debug(f"Changing trace config on device {device_id} {configType}:{configValue}")
out, err = process.communicate(configValue.encode('utf-8'))
if process.returncode != 0:
raise AdbError(
f"Error executing command:\n {configValue}\n\n### OUTPUT ###{out.decode('utf-8')}\n{err.decode('utf-8')}")
log.debug(f"Changing trace config finished on device {device_id}")
server.respond(HTTPStatus.OK, b'', "text/plain")
class ConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
try:
requested_configs = self.get_request(server)
config = SurfaceFlingerTraceConfig()
for requested_config in requested_configs:
if not config.is_valid(requested_config):
raise BadRequest(
f"Unsupported config {requested_config}\n")
config.add(requested_config)
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
command = config.command()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", command)
def add_selected_request_to_config(self, server, device_id, config):
try:
requested_configs = self.get_request(server)
for requested_config in requested_configs:
if config.is_valid(requested_config):
config.add(requested_config, requested_configs[requested_config])
else:
raise BadRequest(
f"Unsupported config {requested_config}\n")
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
if device_id in TRACE_THREADS:
BadRequest(f"Trace in progress for {device_id}")
if not check_root(device_id):
raise AdbError(
f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
return config
class SurfaceFlingerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = SurfaceFlingerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
shell = ['adb', '-s', device_id, 'shell']
log.debug(f"Starting shell {' '.join(shell)}")
execute_command(server, device_id, shell, "sf buffer size", setBufferSize)
class WindowManagerSelectedConfigTrace(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
config = WindowManagerTraceSelectedConfig()
config = add_selected_request_to_config(self, server, device_id, config)
setBufferSize = config.setBufferSize()
setTracingType = config.setTracingType()
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)
class StatusEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
if device_id not in TRACE_THREADS:
raise BadRequest("No trace in progress for {}".format(device_id))
TRACE_THREADS[device_id].reset_timer()
server.respond(HTTPStatus.OK, str(TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
server.respond(HTTPStatus.OK, str(
TRACE_THREADS[device_id].is_alive()).encode("utf-8"), "text/plain")
class DumpEndpoint(DeviceRequestEndpoint):
def process_with_device(self, server, path, device_id):
try:
length = int(server.headers["Content-Length"])
except KeyError as err:
raise BadRequest("Missing Content-Length header\n" + str(err))
except ValueError as err:
raise BadRequest("Content length unreadable\n" + str(err))
try:
requested_types = json.loads(server.rfile.read(length).decode("utf-8"))
requested_types = self.get_request(server)
requested_traces = [DUMP_TARGETS[t] for t in requested_types]
except KeyError as err:
raise BadRequest("Unsupported trace target\n" + str(err))
@@ -510,12 +814,24 @@ class DumpEndpoint(DeviceRequestEndpoint):
class ADBWinscopeProxy(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.router = RequestRouter(self)
self.router.register_endpoint(RequestType.GET, "devices", ListDevicesEndpoint())
self.router.register_endpoint(RequestType.GET, "status", StatusEndpoint())
self.router.register_endpoint(RequestType.GET, "fetch", FetchFileEndpoint())
listDevicesEndpoint = ListDevicesEndpoint()
self.router.register_endpoint(
RequestType.GET, "devices", listDevicesEndpoint)
self.router.register_endpoint(
RequestType.GET, "status", StatusEndpoint())
self.router.register_endpoint(
RequestType.GET, "fetch", FetchFilesEndpoint())
self.router.register_endpoint(RequestType.POST, "start", StartTrace())
self.router.register_endpoint(RequestType.POST, "end", EndTrace())
self.router.register_endpoint(RequestType.POST, "dump", DumpEndpoint())
self.router.register_endpoint(
RequestType.POST, "configtrace", ConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedsfconfigtrace", SurfaceFlingerSelectedConfigTrace())
self.router.register_endpoint(
RequestType.POST, "selectedwmconfigtrace", WindowManagerSelectedConfigTrace())
self.router.register_endpoint(
RequestType.GET, "checkwayland", CheckWaylandServiceEndpoint(listDevicesEndpoint))
super().__init__(request, client_address, server)
def respond(self, code: int, data: bytes, mime: str) -> None:

View File

@@ -244,4 +244,4 @@ describe("DiffGenerator", () => {
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
});
});
});

View File

@@ -155,4 +155,4 @@ describe("ObjectTransformer", () => {
expect(transformedObj).toEqual(expectedTransformedObj);
});
});
});

View File

@@ -25,4 +25,4 @@ describe("Proto Transformations", () => {
}
}
});
});
});

View File

@@ -68,4 +68,4 @@ describe("Error Transformation", () => {
expect(data.entries[1].errors).toEqual([new Error("","",66,"",66)]);
expect(data.entries[2].errors).toEqual([new Error("","",99,"",99)]);
})
});
});

View File

@@ -1,7 +1,7 @@
import { Buffer, RectF, Transform, Matrix, Color, Rect, Region } from '../../src/flickerlib/common.js';
import { ActiveBuffer, RectF, Transform, Matrix33, Color, Rect, Region } from '../../src/flickerlib/common.js';
import { VISIBLE_CHIP } from '../../src/flickerlib/treeview/Chips';
const standardTransform = new Transform(0, new Matrix(1, 0, 0, 0, 1, 0));
const standardTransform = new Transform(0, new Matrix33(1, 0, 0, 0, 1, 0));
const standardRect = new Rect(0, 0, 0, 0);
const standardColor = new Color(0, 0, 0, 1);
const standardCrop = new Rect(0, 0, -1, -1);
@@ -23,13 +23,13 @@ const expectedEmptyRegionLayer = {
z: -1,
zOrderRelativeOf: null,
parentId: 579,
activeBuffer: new Buffer(1440, 2614, 1472, 1),
activeBuffer: new ActiveBuffer(1440, 2614, 1472, 1),
bufferTransform: standardTransform,
color: new Color(0, 0, 0, 0.0069580078125),
crop: standardCrop,
hwcFrame: standardRect,
screenBounds: new RectF(37, 43, 146, 152),
transform: new Transform(0, new Matrix(1, 0, 37.37078094482422, 0, 1, -3.5995326042175293)),
transform: new Transform(0, new Matrix33(1, 0, 37.37078094482422, 0, 1, -3.5995326042175293)),
visibleRegion: new Region([new Rect(37, 43, 146, 152)]),
};
const emptyRegionProto = {
@@ -114,7 +114,7 @@ const expectedInvalidLayerVisibilityLayer = {
zOrderRelativeOf: null,
parentId: 1535,
stableId: "BufferLayer 1536 com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity#2",
activeBuffer: new Buffer(1440, 2880, 1472, 1),
activeBuffer: new ActiveBuffer(1440, 2880, 1472, 1),
bufferTransform: standardTransform,
color: new Color(-1, -1, -1, 0),
hwcFrame: standardRect,
@@ -202,13 +202,13 @@ const expectedOrphanLayersLayer = {
zOrderRelativeOf: null,
parentId: 1011,
stableId: "BufferLayer 1012 SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0",
activeBuffer: new Buffer(1440, 2614, 1472, 1),
activeBuffer: new ActiveBuffer(1440, 2614, 1472, 1),
bufferTransform: standardTransform,
color: standardColor,
crop: standardCrop,
hwcFrame: standardRect,
screenBounds: new RectF(0, 98, 1440, 2712),
transform: new Transform(0, new Matrix(1, 0, 0, 0, 1, 98)),
transform: new Transform(0, new Matrix33(1, 0, 0, 0, 1, 98)),
visibleRegion: new Region([new Rect(0, 98, 1440, 2712)]),
};
const expectedOrphanLayersProto = {
@@ -294,7 +294,7 @@ const expectedRootLayer = {
zOrderRelativeOf: null,
parentId: 12541,
stableId: "BufferQueueLayer 12545 com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp.SimpleActivity#0",
activeBuffer: new Buffer(1440, 2960, 1472, 1),
activeBuffer: new ActiveBuffer(1440, 2960, 1472, 1),
chips: [VISIBLE_CHIP],
bufferTransform: standardTransform,
color: standardColor,
@@ -398,7 +398,7 @@ const expectedRootAospLayer = {
z: 0,
zOrderRelativeOf: null,
parentId: 41,
activeBuffer: new Buffer(1440, 2880, 1472, 1),
activeBuffer: new ActiveBuffer(1440, 2880, 1472, 1),
bufferTransform: standardTransform,
chips: [VISIBLE_CHIP],
color: standardColor,

View File

@@ -55,4 +55,4 @@ function toPlainObject(theClass) {
}
}
export { Node, DiffNode, ObjNode, ObjDiffNode, toPlainObject };
export { Node, DiffNode, ObjNode, ObjDiffNode, toPlainObject };

View File

@@ -38,4 +38,4 @@ export default {
},
}
}
</script>
</script>

View File

@@ -14,9 +14,28 @@
-->
<template>
<div id="app">
<vue-title :appName="appName" :traceName="traceNameForTitle" />
<md-dialog-prompt
class="edit-trace-name-dialog"
:md-active.sync="editingTraceName"
v-model="traceName"
md-title="Edit trace name"
md-input-placeholder="Enter a new trace name"
md-confirm-text="Save" />
<md-app>
<md-app-toolbar md-tag="md-toolbar" class="top-toolbar">
<h1 class="md-title" style="flex: 1">{{title}}</h1>
<h1 class="md-title">{{appName}}</h1>
<div class="trace-name" v-if="dataLoaded">
<div>
<span>{{ traceName }}</span>
<!-- <input type="text" class="trace-name-editable" v-model="traceName" /> -->
<md-icon class="edit-trace-name-btn" @click.native="editTraceName()">edit</md-icon>
</div>
</div>
<md-button
class="md-primary md-theme-default download-all-btn"
@click="generateTags()"
@@ -24,7 +43,7 @@
>Generate Tags</md-button>
<md-button
class="md-primary md-theme-default"
@click="downloadAsZip(files)"
@click="downloadAsZip(files, traceName)"
v-if="dataLoaded"
>Download All</md-button>
<md-button
@@ -39,11 +58,11 @@
<section class="data-inputs" v-if="!dataLoaded">
<div class="input">
<dataadb class="adbinput" ref="adb" :store="store"
@dataReady="onDataReady" @statusChange="setStatus" />
@dataReady="onDataReady" />
</div>
<div class="input" @dragover.prevent @drop.prevent>
<datainput class="fileinput" ref="input" :store="store"
@dataReady="onDataReady" @statusChange="setStatus" />
@dataReady="onDataReady" />
</div>
</section>
@@ -92,6 +111,7 @@ import Searchbar from './Searchbar.vue';
import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts';
import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js';
import { TaggingEngine } from './flickerlib/common';
import titleComponent from './Title.vue';
const APP_NAME = 'Winscope';
@@ -102,7 +122,7 @@ export default {
mixins: [FileType, SaveAsZip, FocusedDataViewFinder],
data() {
return {
title: APP_NAME,
appName: APP_NAME,
activeDataView: null,
// eslint-disable-next-line new-cap
store: LocalStore('app', {
@@ -124,12 +144,14 @@ export default {
presentErrors: [],
searchTypes: [SEARCH_TYPE.TIMESTAMP],
hasTagOrErrorTraces: false,
traceName: "unnamed_winscope_trace",
editingTraceName: false
};
},
created() {
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('scroll', this.onScroll);
document.title = this.title;
// document.title = this.traceName;
},
destroyed() {
window.removeEventListener('keydown', this.onKeyDown);
@@ -203,7 +225,7 @@ export default {
this.store.showFileTypes = [];
this.tagFile = null;
this.$store.commit('clearFiles');
this.buttonClicked("Clear")
this.recordButtonClickedEvent("Clear")
},
onDataViewFocus(file) {
this.$store.commit('setActiveFile', file);
@@ -226,7 +248,8 @@ export default {
event.preventDefault();
return true;
},
onDataReady(files) {
onDataReady(traceName, files) {
this.traceName = traceName;
this.$store.dispatch('setFiles', files);
this.tagFile = this.tagFiles[0] ?? null;
@@ -253,7 +276,7 @@ export default {
},
generateTags() {
// generate tag file
this.buttonClicked("Generate Tags");
this.recordButtonClickedEvent("Generate Tags");
const engine = new TaggingEngine(
this.$store.getters.tagGenerationWmTrace,
this.$store.getters.tagGenerationSfTrace,
@@ -281,6 +304,10 @@ export default {
FILE_TYPES.TAG_TRACE
);
},
editTraceName() {
this.editingTraceName = true;
}
},
computed: {
files() {
@@ -321,11 +348,18 @@ export default {
return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER)
&& fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER);
},
traceNameForTitle() {
if (!this.dataLoaded) {
return undefined;
} else {
return this.traceName;
}
}
},
watch: {
title() {
document.title = this.title;
},
// title() {
// document.title = this.title;
// },
},
components: {
overlay: Overlay,
@@ -333,6 +367,7 @@ export default {
datainput: DataInput,
dataadb: DataAdb,
searchbar: Searchbar,
["vue-title"]: titleComponent,
},
};
</script>
@@ -429,4 +464,37 @@ h1 {
hyphens: auto;
padding: 10px 10px 10px 10px;
}
</style>
.trace-name {
flex: 1;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
font-family: 'Open Sans', sans-serif;
font-size: 1rem;
}
.md-icon.edit-trace-name-btn {
color: rgba(0, 0, 0, 0.6)!important;
font-size: 1rem!important;
margin-bottom: 0.1rem;
}
.md-icon.edit-trace-name-btn:hover {
cursor: pointer;
}
.trace-name-editable {
all: unset;
cursor: default;
}
.edit-trace-name-dialog .md-dialog-container {
min-width: 350px;
}
.md-overlay.md-dialog-overlay {
z-index: 10;
}
</style>

View File

@@ -39,7 +39,7 @@
<md-icon class="md-accent">update</md-icon>
<span class="md-subheading">The version of Winscope ADB Connect proxy running on your machine is incopatibile with Winscope.</span>
<div class="md-body-2">
<p>Please update the proxy to version {{ WINSCOPE_PROXY_VERSION }}</p>
<p>Please update the proxy to version {{ proxyClient.VERSION }}</p>
<p>Run:</p>
<pre>python3 $ANDROID_BUILD_TOP/development/tools/winscope/adb_proxy/winscope_proxy.py</pre>
<p>Or get it from the AOSP repository.</p>
@@ -54,7 +54,7 @@
<span class="md-subheading">Proxy authorisation required</span>
<md-field>
<label>Enter Winscope proxy token</label>
<md-input v-model="adbStore.proxyKey"></md-input>
<md-input v-model="proxyClient.store.proxyKey"></md-input>
</md-field>
<div class="md-body-2">The proxy token is printed to console on proxy launch, copy and paste it above.</div>
<div class="md-layout">
@@ -62,9 +62,9 @@
</div>
</md-card-content>
<md-card-content v-if="status === STATES.DEVICES">
<div class="md-subheading">{{ Object.keys(devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div>
<div class="md-subheading">{{ Object.keys(proxyClient.devices).length > 0 ? "Connected devices:" : "No devices detected" }}</div>
<md-list>
<md-list-item v-for="(device, id) in devices" :key="id" @click="selectDevice(id)" :disabled="!device.authorised">
<md-list-item v-for="(device, id) in proxyClient.devices" :key="id" @click="proxyClient.selectDevice(id)" :disabled="!device.authorised">
<md-icon>{{ device.authorised ? "smartphone" : "screen_lock_portrait" }}</md-icon>
<span class="md-list-item-text">{{ device.authorised ? device.model : "unauthorised" }} ({{ id }})</span>
</md-list-item>
@@ -76,7 +76,7 @@
<md-list>
<md-list-item>
<md-icon>smartphone</md-icon>
<span class="md-list-item-text">{{ devices[selectedDevice].model }} ({{ selectedDevice }})</span>
<span class="md-list-item-text">{{ proxyClient.devices[proxyClient.selectedDevice].model }} ({{ proxyClient.selectedDevice }})</span>
</md-list-item>
</md-list>
<md-button class="md-primary" @click="resetLastDevice">Change device</md-button>
@@ -84,12 +84,12 @@
<div class="trace-section">
<h3>Trace targets:</h3>
<div class="selection">
<md-checkbox class="md-primary" v-for="traceKey in Object.keys(TRACES)" :key="traceKey" v-model="adbStore[traceKey]">{{TRACES[traceKey].name}}</md-checkbox>
<md-checkbox class="md-primary" v-for="traceKey in Object.keys(DYNAMIC_TRACES)" :key="traceKey" v-model="traceStore[traceKey]">{{ DYNAMIC_TRACES[traceKey].name }}</md-checkbox>
</div>
<div class="trace-config">
<h4>Surface Flinger config</h4>
<div class="selection">
<md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="adbStore[config]">{{config}}</md-checkbox>
<md-checkbox class="md-primary" v-for="config in TRACE_CONFIG['layers_trace']" :key="config" v-model="traceStore[config]">{{config}}</md-checkbox>
<div class="selection">
<md-field class="config-selection" v-for="selectConfig in Object.keys(SF_SELECTED_CONFIG)" :key="selectConfig">
<md-select v-model="SF_SELECTED_CONFIG_VALUES[selectConfig]" :placeholder="selectConfig">
@@ -116,7 +116,7 @@
<div class="dump-section">
<h3>Dump targets:</h3>
<div class="selection">
<md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="adbStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox>
<md-checkbox class="md-primary" v-for="dumpKey in Object.keys(DUMPS)" :key="dumpKey" v-model="traceStore[dumpKey]">{{DUMPS[dumpKey].name}}</md-checkbox>
</div>
<div class="md-layout">
<md-button class="md-primary dump-btn" @click="dumpState">Dump state</md-button>
@@ -145,63 +145,45 @@
</flat-card>
</template>
<script>
import {FILE_DECODERS, FILE_TYPES} from './decode.js';
import LocalStore from './localstore.js';
import FlatCard from './components/FlatCard.vue';
import {proxyClient, ProxyState, ProxyEndpoint} from './proxyclient/ProxyClient.ts';
const STATES = {
ERROR: 0,
CONNECTING: 1,
NO_PROXY: 2,
INVALID_VERSION: 3,
UNAUTH: 4,
DEVICES: 5,
START_TRACE: 6,
END_TRACE: 7,
LOAD_DATA: 8,
};
const WINSCOPE_PROXY_VERSION = '0.8';
const WINSCOPE_PROXY_URL = 'http://localhost:5544';
const PROXY_ENDPOINTS = {
DEVICES: '/devices/',
START_TRACE: '/start/',
END_TRACE: '/end/',
CONFIG_TRACE: '/configtrace/',
SELECTED_WM_CONFIG_TRACE: '/selectedwmconfigtrace/',
SELECTED_SF_CONFIG_TRACE: '/selectedsfconfigtrace/',
DUMP: '/dump/',
FETCH: '/fetch/',
STATUS: '/status/',
};
// trace options should be added in a nested category
const TRACES = {
'window_trace': {
name: 'Window Manager',
'default': {
'window_trace': {
name: 'Window Manager',
},
'accessibility_trace': {
name: 'Accessibility',
},
'layers_trace': {
name: 'Surface Flinger',
},
'transactions': {
name: 'Transaction',
},
'proto_log': {
name: 'ProtoLog',
},
'screen_recording': {
name: 'Screen Recording',
},
'ime_trace_clients': {
name: 'Input Method Clients',
},
'ime_trace_service': {
name: 'Input Method Service',
},
'ime_trace_managerservice': {
name: 'Input Method Manager Service',
},
},
'accessibility_trace': {
name: 'Accessibility',
},
'layers_trace': {
name: 'Surface Flinger',
},
'transaction': {
name: 'Transactions',
},
'proto_log': {
name: 'ProtoLog',
},
'screen_recording': {
name: 'Screen Recording',
},
'ime_trace_clients': {
name: 'Input Method Clients',
},
'ime_trace_service': {
name: 'Input Method Service',
},
'ime_trace_managerservice': {
name: 'Input Method Manager Service',
'arc': {
'wayland_trace': {
name: 'Wayland',
},
},
};
@@ -210,6 +192,7 @@ const TRACE_CONFIG = {
'composition',
'metadata',
'hwc',
'tracebuffers',
],
};
@@ -234,8 +217,8 @@ const WM_SELECTED_CONFIG = {
'transaction',
],
'tracinglevel': [
'all',
'trim',
'verbose',
'debug',
'critical',
],
};
@@ -249,56 +232,32 @@ const DUMPS = {
},
};
const proxyFileTypeAdapter = {
'window_trace': FILE_TYPES.WINDOW_MANAGER_TRACE,
'accessibility_trace': FILE_TYPES.ACCESSIBILITY_TRACE,
'layers_trace': FILE_TYPES.SURFACE_FLINGER_TRACE,
'wl_trace': FILE_TYPES.WAYLAND_TRACE,
'layers_dump': FILE_TYPES.SURFACE_FLINGER_DUMP,
'window_dump': FILE_TYPES.WINDOW_MANAGER_DUMP,
'wl_dump': FILE_TYPES.WAYLAND_DUMP,
'screen_recording': FILE_TYPES.SCREEN_RECORDING,
'transactions': FILE_TYPES.TRANSACTIONS_TRACE,
'proto_log': FILE_TYPES.PROTO_LOG,
'system_ui_trace': FILE_TYPES.SYSTEM_UI,
'launcher_trace': FILE_TYPES.LAUNCHER,
'ime_trace_clients': FILE_TYPES.IME_TRACE_CLIENTS,
'ime_trace_service': FILE_TYPES.IME_TRACE_SERVICE,
'ime_trace_managerservice': FILE_TYPES.IME_TRACE_MANAGERSERVICE,
};
const CONFIGS = Object.keys(TRACE_CONFIG).flatMap((file) => TRACE_CONFIG[file]);
export default {
name: 'dataadb',
data() {
return {
STATES,
proxyClient,
ProxyState,
STATES: ProxyState,
TRACES,
DYNAMIC_TRACES: TRACES['default'],
TRACE_CONFIG,
SF_SELECTED_CONFIG,
WM_SELECTED_CONFIG,
SF_SELECTED_CONFIG_VALUES: {},
WM_SELECTED_CONFIG_VALUES: {},
DUMPS,
FILE_DECODERS,
WINSCOPE_PROXY_VERSION,
status: STATES.CONNECTING,
status: ProxyState.CONNECTING,
dataFiles: [],
devices: {},
selectedDevice: '',
refresh_worker: null,
keep_alive_worker: null,
errorText: '',
loadProgress: 0,
adbStore: LocalStore(
'adb',
traceStore: LocalStore(
'trace',
Object.assign(
{
proxyKey: '',
lastDevice: '',
},
Object.keys(TRACES)
this.getAllTraceKeys(TRACES)
.concat(Object.keys(DUMPS))
.concat(CONFIGS)
.reduce(function(obj, key) {
@@ -307,6 +266,10 @@ export default {
),
),
downloadProxyUrl: 'https://android.googlesource.com/platform/development/+/master/tools/winscope/adb_proxy/winscope_proxy.py',
onStateChangeFn: (newState, errorText) => {
this.status = newState;
this.errorText = errorText;
},
};
},
props: ['store'],
@@ -314,37 +277,40 @@ export default {
'flat-card': FlatCard,
},
methods: {
getDevices() {
if (this.status !== STATES.DEVICES && this.status !== STATES.CONNECTING) {
clearInterval(this.refresh_worker);
this.refresh_worker = null;
return;
getAllTraceKeys(traces) {
let keys = [];
for (let dict_key in traces) {
for (let key in traces[dict_key]) {
keys.push(key);
}
}
this.callProxy('GET', PROXY_ENDPOINTS.DEVICES, this, function(request, view) {
return keys;
},
setAvailableTraces() {
this.DYNAMIC_TRACES = this.TRACES['default'];
proxyClient.call('GET', ProxyEndpoint.CHECK_WAYLAND, this, function(request, view) {
try {
view.devices = JSON.parse(request.responseText);
if (view.adbStore.lastDevice && view.devices[view.adbStore.lastDevice] && view.devices[view.adbStore.lastDevice].authorised) {
view.selectDevice(view.adbStore.lastDevice);
} else {
if (view.refresh_worker === null) {
view.refresh_worker = setInterval(view.getDevices, 1000);
}
view.status = STATES.DEVICES;
if(request.responseText == 'true') {
view.appendOptionalTraces('arc');
}
} catch (err) {
} catch(err) {
console.error(err);
view.errorText = request.responseText;
view.status = STATES.ERROR;
proxyClient.setState(ProxyState.ERROR, request.responseText);
}
});
},
appendOptionalTraces(device_key) {
for(let key in this.TRACES[device_key]) {
this.$set(this.DYNAMIC_TRACES, key, this.TRACES[device_key][key]);
}
},
keepAliveTrace() {
if (this.status !== STATES.END_TRACE) {
if (this.status !== ProxyState.END_TRACE) {
clearInterval(this.keep_alive_worker);
this.keep_alive_worker = null;
return;
}
this.callProxy('GET', `${PROXY_ENDPOINTS.STATUS}${this.deviceId()}/`, this, function(request, view) {
proxyClient.call('GET', `${ProxyEndpoint.STATUS}${proxyClient.deviceId()}/`, this, function(request, view) {
if (request.responseText !== 'True') {
view.endTrace();
} else if (view.keep_alive_worker === null) {
@@ -358,84 +324,49 @@ export default {
const requestedSelectedSfConfig = this.toSelectedSfTraceConfig();
const requestedSelectedWmConfig = this.toSelectedWmTraceConfig();
if (requested.length < 1) {
this.errorText = 'No targets selected';
this.status = STATES.ERROR;
this.newEventOccurred("No targets selected");
proxyClient.setState(ProxyState.ERROR, 'No targets selected');
this.recordNewEvent("No targets selected");
return;
}
this.newEventOccurred("Start Trace");
this.callProxy('POST', `${PROXY_ENDPOINTS.CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedConfig);
this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_SF_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedSfConfig);
this.callProxy('POST', `${PROXY_ENDPOINTS.SELECTED_WM_CONFIG_TRACE}${this.deviceId()}/`, this, null, null, requestedSelectedWmConfig);
this.status = STATES.END_TRACE;
this.callProxy('POST', `${PROXY_ENDPOINTS.START_TRACE}${this.deviceId()}/`, this, function(request, view) {
this.recordNewEvent("Start Trace");
proxyClient.call('POST', `${ProxyEndpoint.CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedConfig);
proxyClient.call('POST', `${ProxyEndpoint.SELECTED_SF_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedSfConfig);
proxyClient.call('POST', `${ProxyEndpoint.SELECTED_WM_CONFIG_TRACE}${proxyClient.deviceId()}/`, this, null, null, requestedSelectedWmConfig);
proxyClient.setState(ProxyState.END_TRACE);
proxyClient.call('POST', `${ProxyEndpoint.START_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) {
view.keepAliveTrace();
}, null, requested);
},
dumpState() {
this.buttonClicked("Dump State");
this.recordButtonClickedEvent("Dump State");
const requested = this.toDump();
if (requested.length < 1) {
this.errorText = 'No targets selected';
this.status = STATES.ERROR;
this.newEventOccurred("No targets selected");
proxyClient.setState(ProxyState.ERROR, 'No targets selected');
this.recordNewEvent("No targets selected");
return;
}
this.status = STATES.LOAD_DATA;
this.callProxy('POST', `${PROXY_ENDPOINTS.DUMP}${this.deviceId()}/`, this, function(request, view) {
view.loadFile(requested, 0);
proxyClient.setState(ProxyState.LOAD_DATA);
proxyClient.call('POST', `${ProxyEndpoint.DUMP}${proxyClient.deviceId()}/`, this, function(request, view) {
proxyClient.loadFile(requested, 0, "dump", view);
}, null, requested);
},
endTrace() {
this.status = STATES.LOAD_DATA;
this.callProxy('POST', `${PROXY_ENDPOINTS.END_TRACE}${this.deviceId()}/`, this, function(request, view) {
view.loadFile(view.toTrace(), 0);
proxyClient.setState(ProxyState.LOAD_DATA);
proxyClient.call('POST', `${ProxyEndpoint.END_TRACE}${proxyClient.deviceId()}/`, this, function(request, view) {
proxyClient.loadFile(view.toTrace(), 0, "trace", view);
});
this.newEventOccurred("Ended Trace");
},
loadFile(files, idx) {
this.callProxy('GET', `${PROXY_ENDPOINTS.FETCH}${this.deviceId()}/${files[idx]}/`, this, function(request, view) {
try {
const enc = new TextDecoder('utf-8');
const resp = enc.decode(request.response);
const filesByType = JSON.parse(resp);
for (const filetype in filesByType) {
if (filesByType.hasOwnProperty(filetype)) {
const files = filesByType[filetype];
const fileDecoder = FILE_DECODERS[proxyFileTypeAdapter[filetype]];
for (const encodedFileBuffer of files) {
const buffer = Uint8Array.from(atob(encodedFileBuffer), (c) => c.charCodeAt(0));
const data = fileDecoder.decoder(buffer, fileDecoder.decoderParams, fileDecoder.name, view.store);
view.dataFiles.push(data);
view.loadProgress = 100 * (idx + 1) / files.length; // TODO: Update this
}
}
}
if (idx < files.length - 1) {
view.loadFile(files, idx + 1);
} else {
view.$emit('dataReady', view.dataFiles);
}
} catch (err) {
console.error(err);
view.errorText = err;
view.status = STATES.ERROR;
}
}, 'arraybuffer');
this.recordNewEvent("Ended Trace");
},
toTrace() {
return Object.keys(TRACES)
.filter((traceKey) => this.adbStore[traceKey]);
return Object.keys(this.DYNAMIC_TRACES)
.filter((traceKey) => this.traceStore[traceKey]);
},
toTraceConfig() {
return Object.keys(TRACE_CONFIG)
.filter((file) => this.adbStore[file])
.filter((file) => this.traceStore[file])
.flatMap((file) => TRACE_CONFIG[file])
.filter((config) => this.adbStore[config]);
.filter((config) => this.traceStore[config]);
},
toSelectedSfTraceConfig() {
const requestedSelectedConfig = {};
@@ -457,75 +388,38 @@ export default {
},
toDump() {
return Object.keys(DUMPS)
.filter((dumpKey) => this.adbStore[dumpKey]);
},
selectDevice(device_id) {
this.selectedDevice = device_id;
this.adbStore.lastDevice = device_id;
this.status = STATES.START_TRACE;
},
deviceId() {
return this.selectedDevice;
.filter((dumpKey) => this.traceStore[dumpKey]);
},
restart() {
this.buttonClicked("Connect / Retry");
this.status = STATES.CONNECTING;
this.recordButtonClickedEvent("Connect / Retry");
proxyClient.setState(ProxyState.CONNECTING);
},
resetLastDevice() {
this.buttonClicked("Change Device");
this.adbStore.lastDevice = '';
this.recordButtonClickedEvent("Change Device");
this.proxyClient.resetLastDevice();
this.restart();
},
callProxy(method, path, view, onSuccess, type, jsonRequest) {
const request = new XMLHttpRequest();
var view = this;
request.onreadystatechange = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 0) {
view.status = STATES.NO_PROXY;
} else if (this.status === 200) {
if (this.getResponseHeader('Winscope-Proxy-Version') !== WINSCOPE_PROXY_VERSION) {
view.status = STATES.INVALID_VERSION;
} else if (onSuccess) {
onSuccess(this, view);
}
} else if (this.status === 403) {
view.status = STATES.UNAUTH;
} else {
if (this.responseType === 'text' || !this.responseType) {
view.errorText = this.responseText;
} else if (this.responseType === 'arraybuffer') {
view.errorText = String.fromCharCode.apply(null, new Uint8Array(this.response));
}
view.status = STATES.ERROR;
}
};
request.responseType = type || '';
request.open(method, WINSCOPE_PROXY_URL + path);
request.setRequestHeader('Winscope-Token', this.adbStore.proxyKey);
if (jsonRequest) {
const json = JSON.stringify(jsonRequest);
request.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
request.send(json);
} else {
request.send();
}
},
},
created() {
proxyClient.setState(ProxyState.CONNECTING);
this.proxyClient.onStateChange(this.onStateChangeFn);
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('token')) {
this.adbStore.proxyKey = urlParams.get('token');
this.proxyClient.proxyKey = urlParams.get('token');
}
this.getDevices();
this.proxyClient.getDevices();
},
beforeDestroy() {
this.proxyClient.removeOnStateChange(this.onStateChangeFn);
},
watch: {
status: {
handler(st) {
if (st == STATES.CONNECTING) {
this.getDevices();
if (st == ProxyState.CONNECTING) {
this.proxyClient.getDevices();
}
if (st == ProxyState.START_TRACE) {
this.setAvailableTraces();
}
},
},

View File

@@ -19,27 +19,34 @@
<div class="md-title">Open files</div>
</md-card-header>
<md-card-content>
<div class="dropbox">
<md-list style="background: none">
<div class="dropbox" @click="$refs.fileUpload.click()" ref="dropbox">
<md-list
class="uploaded-files"
v-show="Object.keys(dataFiles).length > 0"
>
<md-list-item v-for="file in dataFiles" v-bind:key="file.filename">
<md-icon>{{FILE_ICONS[file.type]}}</md-icon>
<span class="md-list-item-text">{{file.filename}} ({{file.type}})
</span>
<md-button
class="md-icon-button md-accent"
@click="onRemoveFile(file.type)"
@click="e => {
e.stopPropagation()
onRemoveFile(file.type)
}"
>
<md-icon>close</md-icon>
</md-button>
</md-list-item>
</md-list>
<md-progress-spinner
:md-diameter="30"
:md-stroke="3"
md-mode="indeterminate"
v-show="loadingFiles"
class="progress-spinner"
/>
<div class="progress-spinner-wrapper" v-show="loadingFiles">
<md-progress-spinner
:md-diameter="30"
:md-stroke="3"
md-mode="indeterminate"
class="progress-spinner"
/>
</div>
<input
type="file"
@change="onLoadFile"
@@ -49,8 +56,8 @@
v-show="false"
multiple
/>
<p v-if="!dataReady">
Drag your <b>.winscope</b> or <b>.zip</b> file(s) here to begin
<p v-if="!dataReady && !loadingFiles">
Drag your <b>.winscope</b> or <b>.zip</b> file(s) or click here to begin
</p>
</div>
@@ -137,12 +144,19 @@ export default {
snackbarDuration: 3500,
snackbarText: '',
fetchingSnackbarText: 'Fetching files...',
traceName: undefined,
};
},
props: ['store'],
created() {
// Attempt to load files from extension if present
this.loadFilesFromExtension();
},
mounted() {
this.handleDropboxDragEvents();
},
beforeUnmount() {
},
methods: {
showSnackbarMessage(message, duration) {
@@ -152,7 +166,7 @@ export default {
},
hideSnackbarMessage() {
this.showSnackbar = false;
this.buttonClicked("Hide Snackbar Message")
this.recordButtonClickedEvent("Hide Snackbar Message")
},
getFetchFilesLoadingAnimation() {
let frame = 0;
@@ -173,6 +187,32 @@ export default {
},
});
},
handleDropboxDragEvents() {
// Counter used to keep track of when we actually exit the dropbox area
// When we drag over a child of the dropbox area the dragenter event will
// be called again and subsequently the dragleave so we don't want to just
// remove the class on the dragleave event.
let dropboxDragCounter = 0;
console.log(this.$refs["dropbox"])
this.$refs["dropbox"].addEventListener('dragenter', e => {
dropboxDragCounter++;
this.$refs["dropbox"].classList.add('dragover');
});
this.$refs["dropbox"].addEventListener('dragleave', e => {
dropboxDragCounter--;
if (dropboxDragCounter == 0) {
this.$refs["dropbox"].classList.remove('dragover');
}
});
this.$refs["dropbox"].addEventListener('drop', e => {
dropboxDragCounter = 0;
this.$refs["dropbox"].classList.remove('dragover');
});
},
/**
* Attempt to load files from the extension if present.
*
@@ -247,16 +287,31 @@ export default {
let droppedFiles = e.dataTransfer.files;
if(!droppedFiles) return;
// Record analytics event
this.draggedAndDropped(droppedFiles);
this.recordDragAndDropFileEvent(droppedFiles);
this.processFiles(droppedFiles);
},
onLoadFile(e) {
const files = event.target.files || event.dataTransfer.files;
this.uploadedFileThroughFilesystem(files);
this.recordFileUploadEvent(files);
this.processFiles(files);
},
async processFiles(files) {
console.log("Object.keys(this.dataFiles).length", Object.keys(this.dataFiles).length)
// The trace name to use if we manage to load the archive without errors.
let tmpTraceName;
if (Object.keys(this.dataFiles).length > 0) {
// We have already loaded some files so only want to use the name of
// this archive as the name of the trace if we override all loaded files
} else {
// No files have been uploaded yet so if we are uploading only 1 archive
// we want to use it's name as the trace name
if (files.length == 1 && this.isArchive(files[0])) {
tmpTraceName = this.getFileNameWithoutZipExtension(files[0])
}
}
let error;
const decodedFiles = [];
for (const file of files) {
@@ -301,8 +356,12 @@ export default {
}
decodedFileTypes.add(dataType);
const frozenData = Object.freeze(decodedFile.data.data);
delete decodedFile.data.data;
decodedFile.data.data = frozenData;
this.$set(this.dataFiles,
dataType, decodedFile.data);
dataType, Object.freeze(decodedFile.data));
}
// TODO(b/169305853): Remove this once we have magic numbers or another
@@ -314,7 +373,11 @@ export default {
const selectedFile =
this.getMostLikelyCandidateFile(dataType, files);
this.$set(this.dataFiles, dataType, selectedFile);
if (selectedFile.data) {
selectedFile.data = Object.freeze(selectedFile.data);
}
this.$set(this.dataFiles, dataType, Object.freeze(selectedFile));
// Remove selected file from overriden list
const index = files.indexOf(selectedFile);
@@ -325,6 +388,19 @@ export default {
if (overriddenFileTypes.size > 0) {
this.displayFilesOverridenWarning(overriddenFiles);
}
if (tmpTraceName !== undefined) {
this.traceName = tmpTraceName;
}
},
getFileNameWithoutZipExtension(file) {
const fileNameSplitOnDot = file.name.split('.')
if (fileNameSplitOnDot.slice(-1)[0] == 'zip') {
return fileNameSplitOnDot.slice(0,-1).join('.');
} else {
return file.name;
}
},
/**
@@ -444,8 +520,8 @@ export default {
return undefined;
},
async addFile(file) {
const decodedFiles = [];
isArchive(file) {
const type = this.fileType;
const extension = this.getFileExtensions(file);
@@ -454,9 +530,15 @@ export default {
// 'application/zip' because when loaded from the extension the type is
// incorrect. See comment in loadFilesFromExtension() for more
// information.
if (type === 'bugreport' ||
return type === 'bugreport' ||
(type === 'auto' && (extension === 'zip' ||
file.type === 'application/zip'))) {
file.type === 'application/zip'))
},
async addFile(file) {
const decodedFiles = [];
if (this.isArchive(file)) {
const results = await this.decodeArchive(file);
decodedFiles.push(...results);
} else {
@@ -492,6 +574,14 @@ export default {
return {filetype, data};
},
/**
* Decode a zip file
*
* Load all files that can be decoded, even if some failures occur.
* For example, a zip file with an mp4 recorded via MediaProjection
* doesn't include the winscope metadata (b/140855415), but the trace
* files within the zip should be nevertheless readable
*/
async decodeArchive(archive) {
const buffer = await this.readFile(archive);
@@ -500,32 +590,41 @@ export default {
const decodedFiles = [];
let lastError;
for (const filename in content.files) {
if (content.files.hasOwnProperty(filename)) {
const file = content.files[filename];
if (file.dir) {
// Ignore directories
continue;
const file = content.files[filename];
if (file.dir) {
// Ignore directories
continue;
}
const fileBlob = await file.async('blob');
// Get only filename and remove rest of path
fileBlob.name = filename.split('/').slice(-1).pop();
try {
const decodedFile = await this.decodeFile(fileBlob);
decodedFiles.push(decodedFile);
} catch (e) {
if (!(e instanceof UndetectableFileType)) {
lastError = e;
}
const fileBlob = await file.async('blob');
// Get only filename and remove rest of path
fileBlob.name = filename.split('/').slice(-1).pop();
try {
const decodedFile = await this.decodeFile(fileBlob);
decodedFiles.push(decodedFile);
} catch (e) {
if (!(e instanceof UndetectableFileType)) {
throw e;
}
}
console.error(e);
}
}
if (decodedFiles.length == 0) {
if (lastError) {
throw lastError;
}
throw new Error('No matching files found in archive', archive);
} else {
if (lastError) {
this.showSnackbarMessage(
'Unable to parse all files, check log for more details', 3500);
}
}
return decodedFiles;
@@ -534,7 +633,7 @@ export default {
this.$delete(this.dataFiles, typeName);
},
onSubmit() {
this.$emit('dataReady',
this.$emit('dataReady', this.formattedTraceName,
Object.keys(this.dataFiles).map((key) => this.dataFiles[key]));
},
},
@@ -542,6 +641,14 @@ export default {
dataReady: function() {
return Object.keys(this.dataFiles).length > 0;
},
formattedTraceName() {
if (this.traceName === undefined) {
return 'winscope-trace';
} else {
return this.traceName;
}
}
},
components: {
'flat-card': FlatCard,
@@ -550,16 +657,10 @@ export default {
</script>
<style>
.dropbox:hover {
.dropbox:hover, .dropbox.dragover {
background: rgb(224, 224, 224);
}
.dropbox p {
font-size: 1.2em;
text-align: center;
padding: 50px 10px;
}
.dropbox {
outline: 2px dashed #448aff; /* the dash box */
outline-offset: -10px;
@@ -569,9 +670,29 @@ export default {
min-height: 200px; /* minimum height */
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
}
.progress-spinner {
.dropbox p, .dropbox .progress-spinner-wrapper {
font-size: 1.2em;
margin: auto;
}
.progress-spinner-wrapper, .progress-spinner {
width: fit-content;
height: fit-content;
display: block;
}
</style>
.progress-spinner-wrapper {
padding: 1.5rem 0 1.5rem 0;
}
.dropbox .uploaded-files {
background: none!important;
width: 100%;
}
</style>

View File

@@ -57,8 +57,8 @@
:presentErrors="presentErrors"
ref="view"
/>
<transactionsview
v-else-if="isTransactions(file) && isShowFileType(file.type)"
<transactionsviewlegacy
v-else-if="isTransactionsLegacy(file) && isShowFileType(file.type)"
:trace="file"
ref="view"
/>
@@ -87,7 +87,7 @@ import TraceView from '@/TraceView.vue';
import AccessibilityTraceView from '@/AccessibilityTraceView.vue';
import WindowManagerTraceView from '@/WindowManagerTraceView.vue';
import SurfaceFlingerTraceView from '@/SurfaceFlingerTraceView.vue';
import TransactionsView from '@/TransactionsView.vue';
import TransactionsViewLegacy from '@/TransactionsViewLegacy.vue';
import LogView from '@/LogView.vue';
import FileType from '@/mixins/FileType.js';
import FlatCard from '@/components/FlatCard.vue';
@@ -160,7 +160,6 @@ export default {
// Pass click event to parent, so that click event handler can be attached
// to component.
this.$emit('click', e);
this.newEventOccurred(e.toString());
},
/** Filter data view files by current show settings */
updateShowFileTypes() {
@@ -182,7 +181,7 @@ export default {
mixins: [FileType],
components: {
'traceview': TraceView,
'transactionsview': TransactionsView,
'transactionsviewlegacy': TransactionsViewLegacy,
'logview': LogView,
'flat-card': FlatCard,
AccessibilityTraceView,

View File

@@ -46,14 +46,14 @@
<md-field>
<label>Log Levels</label>
<md-select v-model="selectedLogLevels" multiple>
<md-option v-for="level in logLevels" :value="level">{{ level }}</md-option>
<md-option v-for="level in logLevels" :value="level" v-bind:key="level">{{ level }}</md-option>
</md-select>
</md-field>
<md-field>
<label>Tags</label>
<md-select v-model="selectedTags" multiple>
<md-option v-for="tag in tags" :value="tag">{{ tag }}</md-option>
<md-option v-for="tag in tags" :value="tag" v-bind:key="tag">{{ tag }}</md-option>
</md-select>
</md-field>
@@ -98,6 +98,8 @@ export default {
name: 'logview',
data() {
const data = this.file.data;
// Record analytics event
this.recordOpenTraceEvent("ProtoLog");
const tags = new Set();
const sourceFiles = new Set();

View File

@@ -37,7 +37,7 @@ export default {
margin: 0;
padding: 10px 0;
min-width: 10rem;
z-index: 1500;
z-index: 10;
position: fixed;
list-style: none;
box-sizing: border-box;

View File

@@ -196,11 +196,11 @@
>
<md-icon v-if="minimized">
expand_less
<md-tooltip md-direction="top" @click="buttonClicked(`Expand Timeline`)">Expand timeline</md-tooltip>
<md-tooltip md-direction="top" @click="recordButtonClickedEvent(`Expand Timeline`)">Expand timeline</md-tooltip>
</md-icon>
<md-icon v-else>
expand_more
<md-tooltip md-direction="top" @click="buttonClicked(`Collapse Timeline`)">Collapse timeline</md-tooltip>
<md-tooltip md-direction="top" @click="recordButtonClickedEvent(`Collapse Timeline`)">Collapse timeline</md-tooltip>
</md-icon>
</md-button>
</div>
@@ -503,7 +503,7 @@ export default {
methods: {
toggleSearch() {
this.search = !(this.search);
this.buttonClicked("Toggle Search Bar");
this.recordButtonClickedEvent("Toggle Search Bar");
},
/**
* determines whether left/right arrow keys should move cursor in input field
@@ -523,7 +523,7 @@ export default {
const closestTimestamp = getClosestTimestamp(this.searchInput, this.mergedTimeline.timeline);
this.$store.dispatch("updateTimelineTime", closestTimestamp);
this.updateInputMode(false);
this.newEventOccurred("Searching for timestamp")
this.recordNewEvent("Searching for timestamp")
},
emitBottomHeightUpdate() {
@@ -638,15 +638,15 @@ export default {
},
closeVideoOverlay() {
this.showVideoOverlay = false;
this.buttonClicked("Close Video Overlay")
this.recordButtonClickedEvent("Close Video Overlay")
},
openVideoOverlay() {
this.showVideoOverlay = true;
this.buttonClicked("Open Video Overlay")
this.recordButtonClickedEvent("Open Video Overlay")
},
toggleVideoOverlay() {
this.showVideoOverlay = !this.showVideoOverlay;
this.buttonClicked("Toggle Video Overlay")
this.recordButtonClickedEvent("Toggle Video Overlay")
},
videoLoaded() {
this.$refs.videoOverlay.contentLoaded();
@@ -686,7 +686,7 @@ export default {
navigationStyleFilter =
(f) => f.type === fileType;
}
this.recordChangedNavigationStyleEvent(this.navigationStyle);
this.$store.commit('setNavigationFilesFilter', navigationStyleFilter);
},
updateVideoOverlayWidth(width) {
@@ -743,6 +743,7 @@ export default {
.overlay-content {
flex-grow: 1;
z-index: 10;
}
.bottom-nav {

View File

@@ -15,14 +15,23 @@
<template>
<div class="bounds" :style="boundsStyle">
<div
class="rect" v-for="r in filteredRects"
:style="rectToStyle(r)"
@click="onClick(r)"
v-bind:key="`${r.left}-${r.right}-${r.top}-${r.bottom}-${r.ref.name}`"
class="rect" v-for="rect in filteredRects"
:style="rectToStyle(rect)"
@click="onClick(rect)"
v-bind:key="`${rect.left}-${rect.right}-${rect.top}-${rect.bottom}-${rect.ref.name}`"
>
<span class="label">{{r.label}}</span>
<span class="label">{{rect.label}}</span>
</div>
<div class="highlight" v-if="highlight" :style="rectToStyle(highlight)" />
<div
class="highlight"
v-if="highlight"
:style="rectToStyle(highlight)"
/>
<div
class="displayRect" v-for="rect in displayRects"
:style="rectToStyle(rect)"
v-bind:key="`${rect.left}-${rect.right}-${rect.top}-${rect.bottom}-${rect.id}`"
/>
</div>
</template>
@@ -33,7 +42,7 @@ import {multiplyRect} from './matrix_utils.js';
export default {
name: 'rects',
props: ['bounds', 'rects', 'highlight'],
props: ['bounds', 'rects', 'highlight','displays'],
data() {
return {
desiredHeight: 800,
@@ -45,12 +54,27 @@ export default {
if (this.bounds) {
return this.bounds;
}
const width = Math.max(
...this.rects.map((r) => multiplyRect(r.transform, r).right));
const height = Math.max(
...this.rects.map((r) => multiplyRect(r.transform, r).bottom));
var width = Math.max(
...this.rects.map((rect) => multiplyRect(rect.transform, rect).right));
var height = Math.max(
...this.rects.map((rect) => multiplyRect(rect.transform, rect).bottom));
// constrain max bounds to prevent boundless layers from shrinking visible displays
if (this.hasDisplays) {
width = Math.min(width, this.maxWidth);
height = Math.min(height, this.maxHeight);
}
return {width, height};
},
maxWidth() {
return Math.max(...this.displayRects.map(rect => rect.width)) * 1.3;
},
maxHeight() {
return Math.max(...this.displayRects.map(rect => rect.height)) * 1.3;
},
hasDisplays() {
return this.displays.length > 0;
},
boundsStyle() {
return this.rectToStyle({top: 0, left: 0, right: this.boundsC.width,
bottom: this.boundsC.height});
@@ -58,11 +82,16 @@ export default {
filteredRects() {
return this.rects.filter((rect) => {
const isVisible = rect.ref.isVisible;
console.warn(`Name: ${rect.ref.name}`, `Kind: ${rect.ref.kind}`,
`isVisible=${isVisible}`);
return isVisible;
});
},
displayRects() {
return this.displays.map(display => {
var rect = display.layerStackSpace;
rect.id = display.id;
return rect;
});
},
},
methods: {
s(sourceCoordinate) { // translate source into target coordinates
@@ -74,17 +103,17 @@ export default {
}
return sourceCoordinate * scale;
},
rectToStyle(r) {
const x = this.s(r.left);
const y = this.s(r.top);
const w = this.s(r.right) - this.s(r.left);
const h = this.s(r.bottom) - this.s(r.top);
rectToStyle(rect) {
const x = this.s(rect.left);
const y = this.s(rect.top);
const w = this.s(rect.right) - this.s(rect.left);
const h = this.s(rect.bottom) - this.s(rect.top);
let t;
if (r.transform && r.transform.matrix) {
t = r.transform.matrix;
if (rect.transform && rect.transform.matrix) {
t = rect.transform.matrix;
} else {
t = r.transform;
t = rect.transform;
}
const tr = t ? `matrix(${t.dsdx}, ${t.dtdx}, ${t.dsdy}, ${t.dtdy}, ` +
@@ -92,13 +121,10 @@ export default {
const rectStyle = `top: ${y}px; left: ` +
`${x}px; height: ${h}px; width: ${w}px; ` +
`transform: ${tr}; transform-origin: 0 0;`;
if (r && r.ref) {
console.log(`${r.ref.name} - ${rectStyle}`);
}
return rectStyle;
},
onClick(r) {
this.$emit('rect-click', r.ref);
onClick(rect) {
this.$emit('rect-click', rect.ref);
},
},
};
@@ -109,7 +135,7 @@ export default {
position: relative;
overflow: hidden;
}
.highlight, .rect {
.highlight, .rect, .displayRect {
position: absolute;
box-sizing: border-box;
display: flex;
@@ -117,10 +143,15 @@ export default {
}
.rect {
border: 1px solid black;
background-color: rgba(110, 114, 116, 0.8);
background-color: rgba(146, 149, 150, 0.8);
}
.highlight {
border: 2px solid rgb(235, 52, 52);
background-color: rgba(243, 212, 212, 0.25);
pointer-events: none;
}
.displayRect {
border: 4px dashed #195aca;
pointer-events: none;
}
.label {

View File

@@ -102,7 +102,7 @@
class="inline-error"
@click="setCurrentTimestamp(item.timestamp)"
>
{{item.message}}
{{ `${item.assertionName} ${item.message}` }}
</td>
</tr>
</table>

View File

@@ -37,7 +37,14 @@ export default {
const summary = [];
if (layer?.visibilityReason) {
summary.push({key: 'Invisible due to', value: layer.visibilityReason});
let reason = "";
if (Array.isArray(layer.visibilityReason)) {
reason = layer.visibilityReason.join(", ");
} else {
reason = layer.visibilityReason;
}
summary.push({key: 'Invisible due to', value: reason});
}
if (layer?.occludedBy?.length > 0) {

View File

@@ -136,7 +136,7 @@ export default {
cursorMask.style.position = 'fixed';
cursorMask.style.top = '0';
cursorMask.style.left = '0';
cursorMask.style['z-index'] = '1000';
cursorMask.style['z-index'] = '10';
return {
inject: () => {

View File

@@ -166,7 +166,7 @@ export default {
'left': 0,
'height': '100vh',
'width': '100vw',
'z-index': 100,
'z-index': 10,
'cursor': 'crosshair',
});
@@ -350,7 +350,7 @@ export default {
.selection, .selection-intent {
position: absolute;
z-index: 100;
z-index: 10;
background: rgba(255, 36, 36, 0.5);
pointer-events: none;
}

View File

@@ -0,0 +1,31 @@
<script>
export default {
name: 'vue-title',
props: ['appName', 'traceName'],
watch: {
traceName: {
immediate: true,
handler() {
this.updatePageTitle();
}
},
appName: {
immediate: true,
handler() {
this.updatePageTitle();
}
}
},
methods: {
updatePageTitle() {
if (this.traceName == null || this.traceName == "") {
document.title = this.appName;
} else {
document.title = this.traceName;
}
}
},
render () {
},
}
</script>

View File

@@ -18,6 +18,7 @@
<rects
:bounds="bounds"
:rects="rects"
:displays="displays"
:highlight="highlight"
@rect-click="onRectClick"
/>
@@ -188,6 +189,7 @@ export default {
lastSelectedStableId: null,
bounds: {},
rects: [],
displays: [],
item: null,
tree: null,
highlight: null,
@@ -206,12 +208,25 @@ export default {
this.selectedTree = this.getTransformedProperties(item);
this.highlight = item.rect;
this.lastSelectedStableId = item.stableId;
// Record analytics event
if (item.type || item.kind || item.stableId) {
this.recordOpenedEntryEvent(item.type ?? item.kind ?? item.stableId);
}
this.$emit('focus');
},
getTransformedProperties(item) {
ObjectFormatter.displayDefaults = this.displayDefaults;
// There are 2 types of object whose properties can appear in the property
// list: Flicker objects (WM/SF traces) and dictionaries
// (IME/Accessibilty/Transactions).
// While flicker objects have their properties directly in the main object,
// those created by a call to the transform function have their properties
// inside an obj property. This makes both cases work
// TODO(209452852) Refactor both flicker and winscope-native objects to
// implement a common display interface that can be better handled
const target = item.obj ?? item;
const transformer = new ObjectTransformer(
getPropertiesForDisplay(item),
getPropertiesForDisplay(target),
item.name,
stableIdCompatibilityFixup(item),
).setOptions({
@@ -254,6 +269,13 @@ export default {
this.rects = [...rects].reverse();
this.bounds = item.bounds;
//only update displays if item is SF trace and displays present
if (item.stableId==="LayerTraceEntry") {
this.displays = item.displays;
} else {
this.displays = [];
}
this.hierarchySelected = null;
this.selectedTree = null;
this.highlight = null;
@@ -341,7 +363,12 @@ export default {
},
},
created() {
this.setData(this.file.data[this.file.selectedIndex ?? 0]);
const item = this.file.data[this.file.selectedIndex ?? 0];
// Record analytics event
if (item.type || item.kind || item.stableId) {
this.recordOpenTraceEvent(item.type ?? item.kind ?? item.stableId);
}
this.setData(item);
},
destroyed() {
this.store.flickerTraceView = false;
@@ -419,11 +446,11 @@ function getFilter(filterString) {
const negative = [];
filterStrings.forEach((f) => {
if (f.startsWith('!')) {
const str = f.substring(1);
negative.push((s) => s.indexOf(str) === -1);
const regex = new RegExp(f.substring(1), "i");
negative.push((s) => !regex.test(s));
} else {
const str = f;
positive.push((s) => s.indexOf(str) !== -1);
const regex = new RegExp(f, "i");
positive.push((s) => regex.test(s));
}
});
const filter = (item) => {

View File

@@ -69,7 +69,7 @@
import { shortenName } from './flickerlib/mixin'
export default {
name: 'transaction-entry',
name: 'transaction-entry-legacy',
props: {
index: {
type: Number,

View File

@@ -40,15 +40,17 @@
<div class="input">
<div>
<md-autocomplete
v-model="selectedProperty"
:md-options="properties"
>
<label>Changed property</label>
</md-autocomplete>
<!-- TODO(b/159582192): Add way to select value a property has
changed to, figure out how to handle properties that are
objects... -->
<md-field>
<label>Changed property</label>
<md-select v-model="selectedProperties" multiple>
<md-option
v-for="property in properties"
:value="property"
v-bind:key="property">
{{ property }}
</md-option>
</md-select>
</md-field>
</div>
</div>
@@ -150,14 +152,17 @@
<script>
import TreeView from './TreeView.vue';
import VirtualList from '../libs/virtualList/VirtualList';
import TransactionEntry from './TransactionEntry.vue';
import TransactionEntryLegacy from './TransactionEntryLegacy.vue';
import FlatCard from './components/FlatCard.vue';
import {ObjectTransformer} from './transform.js';
import {expandTransactionId} from '@/traces/Transactions.ts';
import {expandTransactionId} from '@/traces/TransactionsLegacy.ts';
/**
* @deprecated This trace has been replaced by the new transactions trace
*/
export default {
name: 'transactionsview',
name: 'transactionsviewlegacy',
props: ['trace'],
data() {
const transactionTypes = new Set();
@@ -197,15 +202,17 @@ export default {
searchInput: '',
selectedTree: null,
filters: [],
selectedProperty: null,
selectedProperties: [],
selectedTransaction: null,
transactionEntryComponent: TransactionEntry,
transactionEntryComponent: TransactionEntryLegacy,
transactionsTrace,
expandTransactionId,
};
},
computed: {
data() {
// Record analytics event
this.recordOpenTraceEvent("TransactionsTrace");
return this.transactionsTrace.data;
},
filteredData() {
@@ -232,10 +239,13 @@ export default {
filteredData = filteredData.filter(
this.filterTransactions((transaction) => {
for (const filter of this.filters) {
if (isNaN(filter) && transaction.layerName?.includes(filter)) {
// If filter isn't a number then check if the transaction's
// target surface's name matches the filter if so keep it.
return true;
if (isNaN(filter)) {
// If filter isn't a number then check if the transaction's
// target surface's name matches the filter if so keep it.
const regexFilter = new RegExp(filter, "i");
if (regexFilter.test(transaction.layerName)) {
return true;
}
}
if (filter == transaction.obj.id) {
// If filteter is a number then check if the filter matches
@@ -250,12 +260,12 @@ export default {
);
}
if (this.selectedProperty) {
if (this.selectedProperties.length > 0) {
const regexFilter = new RegExp(this.selectedProperties.join("|"), "i");
filteredData = filteredData.filter(
this.filterTransactions((transaction) => {
for (const key in transaction.obj) {
if (this.isMeaningfulChange(transaction.obj, key) &&
key === this.selectedProperty) {
if (this.isMeaningfulChange(transaction.obj, key) && regexFilter.test(key)) {
return true;
}
}

View File

@@ -222,7 +222,7 @@ export default {
toggleTree() {
this.setCollapseValue(!this.isCollapsed);
if (!this.isCollapsed) {
this.openedToSeeAttributeField(this.item.name)
this.recordExpandedPropertyEvent(this.item.name)
}
},
expandTree() {

View File

@@ -36,6 +36,8 @@ export default {
name: 'videoview',
props: ['file', 'height'],
data() {
// Record analytics event
this.recordOpenTraceEvent("Video");
return {};
},
computed: {

View File

@@ -22,7 +22,7 @@
</template>
<script>
import Arrow from './Arrow.vue';
import {LocalStore} from '../../localstore.js';
import LocalStore from '../../localstore.js';
var transitionCount = false;

View File

@@ -13,7 +13,6 @@
"resolvedChildren",
"visibilityReason",
"absoluteZ",
"name",
"children",
"stableId"
],
@@ -33,4 +32,4 @@
"WindowConfiguration.orientation": "android.content.pm.ActivityInfo.ScreenOrientation",
"WindowState.orientation": "android.content.pm.ActivityInfo.ScreenOrientation"
}
}
}

View File

@@ -21,7 +21,8 @@ import jsonProtoDefsAccessibility from 'frameworks/base/core/proto/android/serve
import jsonProtoDefsWm from 'frameworks/base/core/proto/android/server/windowmanagertrace.proto';
import jsonProtoDefsProtoLog from 'frameworks/base/core/proto/android/internal/protolog.proto';
import jsonProtoDefsSf from 'frameworks/native/services/surfaceflinger/layerproto/layerstrace.proto';
import jsonProtoDefsTransaction from 'frameworks/native/cmds/surfacereplayer/proto/src/trace.proto';
import jsonProtoDefsTransaction from 'frameworks/native/services/surfaceflinger/layerproto/transactions.proto';
import jsonProtoDefsTransactionLegacy from 'frameworks/native/cmds/surfacereplayer/proto/src/trace.proto';
import jsonProtoDefsWl from 'WaylandSafePath/waylandtrace.proto';
import jsonProtoDefsSysUi from 'frameworks/base/packages/SystemUI/src/com/android/systemui/tracing/sysui_trace.proto';
import jsonProtoDefsLauncher from 'packages/apps/Launcher3/protos/launcher_trace_file.proto';
@@ -31,6 +32,7 @@ import jsonProtoDefsErrors from 'platform_testing/libraries/flicker/src/com/andr
import protobuf from 'protobufjs';
import {transform_accessibility_trace} from './transform_accessibility.js';
import {transform_transaction_trace} from './transform_transaction.js';
import {transform_transaction_trace_legacy} from './transform_transaction_legacy.js';
import {transform_wl_outputstate, transform_wayland_trace} from './transform_wl.js';
import {transformProtolog} from './transform_protolog.js';
import {transform_sysui_trace} from './transform_sys_ui.js';
@@ -42,6 +44,7 @@ import AccessibilityTrace from '@/traces/Accessibility.ts';
import SurfaceFlingerTrace from '@/traces/SurfaceFlinger.ts';
import WindowManagerTrace from '@/traces/WindowManager.ts';
import TransactionsTrace from '@/traces/Transactions.ts';
import TransactionsTraceLegacy from '@/traces/TransactionsLegacy.ts';
import ScreenRecordingTrace from '@/traces/ScreenRecording.ts';
import WaylandTrace from '@/traces/Wayland.ts';
import ProtoLogTrace from '@/traces/ProtoLog.ts';
@@ -63,7 +66,8 @@ const WmTraceMessage = lookup_type(jsonProtoDefsWm, 'com.android.server.wm.Windo
const WmDumpMessage = lookup_type(jsonProtoDefsWm, 'com.android.server.wm.WindowManagerServiceDumpProto');
const SfTraceMessage = lookup_type(jsonProtoDefsSf, 'android.surfaceflinger.LayersTraceFileProto');
const SfDumpMessage = lookup_type(jsonProtoDefsSf, 'android.surfaceflinger.LayersProto');
const SfTransactionTraceMessage = lookup_type(jsonProtoDefsTransaction, 'Trace');
const SfTransactionTraceMessage = lookup_type(jsonProtoDefsTransaction, 'TransactionTraceFile');
const SfTransactionTraceMessageLegacy = lookup_type(jsonProtoDefsTransactionLegacy, 'Trace');
const WaylandTraceMessage = lookup_type(jsonProtoDefsWl, 'org.chromium.arc.wayland_composer.TraceFileProto');
const WaylandDumpMessage = lookup_type(jsonProtoDefsWl, 'org.chromium.arc.wayland_composer.OutputStateProto');
const ProtoLogMessage = lookup_type(jsonProtoDefsProtoLog, 'com.android.internal.protolog.ProtoLogFileProto');
@@ -77,6 +81,7 @@ const ErrorTraceMessage = lookup_type(jsonProtoDefsErrors, 'com.android.server.w
const ACCESSIBILITY_MAGIC_NUMBER = [0x09, 0x41, 0x31, 0x31, 0x59, 0x54, 0x52, 0x41, 0x43]; // .A11YTRAC
const LAYER_TRACE_MAGIC_NUMBER = [0x09, 0x4c, 0x59, 0x52, 0x54, 0x52, 0x41, 0x43, 0x45]; // .LYRTRACE
const TRANSACTIONS_TRACE_MAGIC_NUMBER = [0x09, 0x54, 0x4e, 0x58, 0x54, 0x52, 0x41, 0x43, 0x45]; // .TNXTRACE
const WINDOW_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45]; // .WINTRACE
const MPEG4_MAGIC_NMBER = [0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32]; // ....ftypmp42
const WAYLAND_TRACE_MAGIC_NUMBER = [0x09, 0x57, 0x59, 0x4c, 0x54, 0x52, 0x41, 0x43, 0x45]; // .WYLTRACE
@@ -97,6 +102,7 @@ const FILE_TYPES = Object.freeze({
SURFACE_FLINGER_DUMP: 'SurfaceFlingerDump',
SCREEN_RECORDING: 'ScreenRecording',
TRANSACTIONS_TRACE: 'TransactionsTrace',
TRANSACTIONS_TRACE_LEGACY: 'TransactionsTraceLegacy',
WAYLAND_TRACE: 'WaylandTrace',
WAYLAND_DUMP: 'WaylandDump',
PROTO_LOG: 'ProtoLog',
@@ -130,6 +136,7 @@ const FILE_ICONS = {
[FILE_TYPES.SURFACE_FLINGER_DUMP]: SURFACE_FLINGER_ICON,
[FILE_TYPES.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
[FILE_TYPES.TRANSACTIONS_TRACE]: TRANSACTION_ICON,
[FILE_TYPES.TRANSACTIONS_TRACE_LEGACY]: TRANSACTION_ICON,
[FILE_TYPES.WAYLAND_TRACE]: WAYLAND_ICON,
[FILE_TYPES.WAYLAND_DUMP]: WAYLAND_ICON,
[FILE_TYPES.PROTO_LOG]: PROTO_LOG_ICON,
@@ -152,6 +159,7 @@ const TRACE_TYPES = Object.freeze({
SURFACE_FLINGER: 'SurfaceFlingerTrace',
SCREEN_RECORDING: 'ScreenRecording',
TRANSACTION: 'Transaction',
TRANSACTION_LEGACY: 'Transaction (Legacy)',
WAYLAND: 'Wayland',
PROTO_LOG: 'ProtoLog',
SYSTEM_UI: 'SystemUI',
@@ -196,6 +204,14 @@ const TRACE_INFO = {
],
constructor: TransactionsTrace,
},
[TRACE_TYPES.TRANSACTION_LEGACY]: {
name: 'Transactions (Legacy)',
icon: TRANSACTION_ICON,
files: [
oneOf(FILE_TYPES.TRANSACTIONS_TRACE_LEGACY),
],
constructor: TransactionsTraceLegacy,
},
[TRACE_TYPES.WAYLAND]: {
name: 'Wayland',
icon: WAYLAND_ICON,
@@ -284,6 +300,7 @@ export const TRACE_ICONS = {
[TRACE_TYPES.SURFACE_FLINGER]: SURFACE_FLINGER_ICON,
[TRACE_TYPES.SCREEN_RECORDING]: SCREEN_RECORDING_ICON,
[TRACE_TYPES.TRANSACTION]: TRANSACTION_ICON,
[TRACE_TYPES.TRANSACTION_LEGACY]: TRANSACTION_ICON,
[TRACE_TYPES.WAYLAND]: WAYLAND_ICON,
[TRACE_TYPES.PROTO_LOG]: PROTO_LOG_ICON,
[TRACE_TYPES.SYSTEM_UI]: SYSTEM_UI_ICON,
@@ -396,6 +413,17 @@ const FILE_DECODERS = {
timeline: true,
},
},
[FILE_TYPES.TRANSACTIONS_TRACE_LEGACY]: {
name: 'Transactions (Legacy)',
decoder: protoDecoder,
decoderParams: {
type: FILE_TYPES.TRANSACTIONS_TRACE_LEGACY,
mime: 'application/octet-stream',
objTypeProto: SfTransactionTraceMessageLegacy,
transform: transform_transaction_trace_legacy,
timeline: true,
},
},
[FILE_TYPES.PROTO_LOG]: {
name: 'ProtoLog',
decoder: protoDecoder,
@@ -537,6 +565,7 @@ function decodeAndTransformProto(buffer, params, displayDefaults) {
// From S onwards, returns a LayerTrace object, iterating over multiple items allows
// winscope to handle both the new and legacy formats
// TODO Refactor the decode.js code into a set of decoders to clean up the code
let lastError = null;
for (var x = 0; x < objTypesProto.length; x++) {
const objType = objTypesProto[x];
const transform = transforms[x];
@@ -546,9 +575,14 @@ function decodeAndTransformProto(buffer, params, displayDefaults) {
const transformed = transform(decoded);
return transformed;
} catch (e) {
lastError = e;
// check next parser
}
}
if (lastError) {
throw lastError;
}
throw new UndetectableFileType('Unable to parse file');
}
@@ -637,6 +671,9 @@ function detectAndDecode(buffer, fileName, store) {
if (arrayStartsWith(buffer, MPEG4_MAGIC_NMBER)) {
return decodedFile(FILE_TYPES.SCREEN_RECORDING, buffer, fileName, store);
}
if (arrayStartsWith(buffer, TRANSACTIONS_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.TRANSACTIONS_TRACE, buffer, fileName, store);
}
if (arrayStartsWith(buffer, WAYLAND_TRACE_MAGIC_NUMBER)) {
return decodedFile(FILE_TYPES.WAYLAND_TRACE, buffer, fileName, store);
}
@@ -667,10 +704,10 @@ function detectAndDecode(buffer, fileName, store) {
// TODO(b/169305853): Add magic number at beginning of file for better auto detection
for (const [filetype, condition] of [
[FILE_TYPES.TRANSACTIONS_TRACE, (file) => file.data.length > 0],
[FILE_TYPES.TRANSACTIONS_TRACE_LEGACY, (file) => file.data.length > 0],
[FILE_TYPES.WAYLAND_DUMP, (file) => (file.data.length > 0 && file.data.children[0] > 0) || file.data.length > 1],
[FILE_TYPES.WINDOW_MANAGER_DUMP],
[FILE_TYPES.SURFACE_FLINGER_DUMP],
[FILE_TYPES.SURFACE_FLINGER_DUMP]
]) {
try {
const [, fileData] = decodedFile(filetype, buffer, fileName, store);

View File

@@ -23,7 +23,7 @@ export default abstract class DumpBase implements IDump {
this._files = files;
}
get files(): any[] {
get files(): readonly any[] {
return Object.values(this._files).flat();
}
@@ -31,6 +31,6 @@ export default abstract class DumpBase implements IDump {
}
interface IDump {
files: Array<Object>;
files: readonly Object[];
type: String,
}

View File

@@ -43,4 +43,4 @@ export default class SurfaceFlinger extends DumpBase {
);
return new LayersTrace([entry], source);
}
}
}

View File

@@ -37,4 +37,4 @@ export default class WindowManager extends DumpBase {
const state = WindowManagerTrace.fromDump(proto);
return new WindowManagerTrace([state], source);
}
}
}

View File

@@ -15,12 +15,12 @@
*/
import { LayersTrace } from "./common"
import LayerTraceEntry from './layers/LayerTraceEntry'
import LayerTraceEntryLazy from './layers/LayerTraceEntryLazy'
LayersTrace.fromProto = function (proto: any): LayersTrace {
const entries = []
for (const entryProto of proto.entry) {
const transformedEntry = LayerTraceEntry.fromProto(
const transformedEntry = new LayerTraceEntryLazy(
/* protos */ entryProto.layers.layers,
/* displays */ entryProto.displays,
/* timestamp */ entryProto.elapsedRealtimeNanos,

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {toSize, toBuffer, toColor, toPoint, toRect,
toRectF, toRegion, toTransform} from './common';
import {toSize, toActiveBuffer, toColor, toColor3, toPoint, toRect,
toRectF, toRegion, toMatrix22, toTransform} from './common';
import intDefMapping from
'../../../../../prebuilts/misc/common/winscope/intDefMapping.json';
import config from '../config/Configuration.json'
@@ -99,41 +99,48 @@ export default class ObjectFormatter {
if (value === null || value === undefined) {
if (this.displayDefaults) {
result[key] = value
result[key] = value;
}
return
}
if (value || this.displayDefaults) {
// raw values (e.g., false or 0)
if (!value) {
result[key] = value
// flicker obj
if (value.prettyPrint) {
} else if (value.prettyPrint) {
const isEmpty = value.isEmpty === true;
if (!isEmpty || this.displayDefaults) {
result[key] = value.prettyPrint()
result[key] = value.prettyPrint();
}
} else {
// converted proto to flicker
const translatedObject = this.translateObject(value)
const translatedObject = this.translateObject(value);
if (translatedObject) {
result[key] = translatedObject.prettyPrint()
if (translatedObject.prettyPrint) {
result[key] = translatedObject.prettyPrint();
}
else {
result[key] = translatedObject;
}
// objects - recursive call
} else if (value && typeof(value) == `object`) {
const childObj = this.format(value) as any
const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty
const childObj = this.format(value) as any;
const isEmpty = Object.entries(childObj).length == 0 || childObj.isEmpty;
if (!isEmpty || this.displayDefaults) {
result[key] = childObj
result[key] = childObj;
}
} else {
// values
result[key] = this.translateIntDef(obj, key, value)
result[key] = this.translateIntDef(obj, key, value);
}
}
}
})
// return Object.freeze(result)
return result
return result;
}
/**
@@ -144,23 +151,25 @@ export default class ObjectFormatter {
* @param obj Object to translate
*/
private static translateObject(obj) {
const type = obj?.$type?.name
const type = obj?.$type?.name;
switch(type) {
case `SizeProto`: return toSize(obj)
case `ActiveBufferProto`: return toBuffer(obj)
case `ColorProto`: return toColor(obj)
case `PointProto`: return toPoint(obj)
case `RectProto`: return toRect(obj)
case `FloatRectProto`: return toRectF(obj)
case `RegionProto`: return toRegion(obj)
case `TransformProto`: return toTransform(obj)
case `SizeProto`: return toSize(obj);
case `ActiveBufferProto`: return toActiveBuffer(obj);
case `Color3`: return toColor3(obj);
case `ColorProto`: return toColor(obj);
case `PointProto`: return toPoint(obj);
case `RectProto`: return toRect(obj);
case `Matrix22`: return toMatrix22(obj);
case `FloatRectProto`: return toRectF(obj);
case `RegionProto`: return toRegion(obj);
case `TransformProto`: return toTransform(obj);
case 'ColorTransformProto': {
const formatted = this.formatColorTransform(obj.val);
return `${formatted}`;
}
}
return null
return null;
}
private static formatColorTransform(vals) {
@@ -181,17 +190,17 @@ export default class ObjectFormatter {
* @param propertyName Property to search
*/
private static getTypeDefSpec(obj: any, propertyName: string): string {
const fields = obj?.$type?.fields
const fields = obj?.$type?.fields;
if (!fields) {
return null
return null;
}
const options = fields[propertyName]?.options
const options = fields[propertyName]?.options;
if (!options) {
return null
return null;
}
return options["(.android.typedef)"]
return options["(.android.typedef)"];
}
/**
@@ -204,23 +213,23 @@ export default class ObjectFormatter {
* @param value Property value
*/
private static translateIntDef(parentObj: any, propertyName: string, value: any): string {
const parentClassName = parentObj.constructor.name
const propertyPath = `${parentClassName}.${propertyName}`
const parentClassName = parentObj.constructor.name;
const propertyPath = `${parentClassName}.${propertyName}`;
let translatedValue = value
let translatedValue = value;
// Parse Flicker objects (no intdef annotation supported)
if (this.FLICKER_INTDEF_MAP.has(propertyPath)) {
translatedValue = this.getIntFlagsAsStrings(value,
this.FLICKER_INTDEF_MAP.get(propertyPath))
this.FLICKER_INTDEF_MAP.get(propertyPath));
} else {
// If it's a proto, search on the proto definition for the intdef type
const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName)
const typeDefSpec = this.getTypeDefSpec(parentObj, propertyName);
if (typeDefSpec) {
translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec)
translatedValue = this.getIntFlagsAsStrings(value, typeDefSpec);
}
}
return translatedValue
return translatedValue;
}
/**

View File

@@ -50,7 +50,6 @@ WindowManagerState.fromProto = function (proto: any, timestamp: number = 0, wher
);
addAttributes(entry, proto);
console.warn("Created ", entry.kind, " stableId=", entry.stableId)
return entry
}
@@ -89,7 +88,8 @@ function createWindowManagerPolicy(proto: any): WindowManagerPolicy {
function createRootWindowContainer(proto: any): RootWindowContainer {
const windowContainer = WindowContainer.fromProto(
/* proto */ proto.windowContainer,
/* childrenProto */ proto.windowContainer.children.reverse()
/* childrenProto */ proto.windowContainer?.children?.reverse() ?? [],
/* isActivityInTree */ false
);
if (windowContainer == null) {
@@ -114,4 +114,4 @@ function createKeyguardControllerState(proto: any): KeyguardControllerState {
);
}
export default WindowManagerState;
export default WindowManagerState;

View File

@@ -56,14 +56,18 @@ const WindowToken = require('flicker').com.android.server.wm.traces.common.
// SF
const Layer = require('flicker').com.android.server.wm.traces.common.
layers.Layer;
const BaseLayerTraceEntry = require('flicker').com.android.server.wm.traces.common.
layers.BaseLayerTraceEntry;
const LayerTraceEntry = require('flicker').com.android.server.wm.traces.common.
layers.LayerTraceEntry;
const LayerTraceEntryBuilder = require('flicker').com.android.server.wm.traces.
common.layers.LayerTraceEntryBuilder;
const LayersTrace = require('flicker').com.android.server.wm.traces.common.
layers.LayersTrace;
const Matrix = require('flicker').com.android.server.wm.traces.common.layers.
Transform.Matrix;
const Matrix22 = require('flicker').com.android.server.wm.traces.common
.Matrix22;
const Matrix33 = require('flicker').com.android.server.wm.traces.common
.Matrix33;
const Transform = require('flicker').com.android.server.wm.traces.common.
layers.Transform;
const Display = require('flicker').com.android.server.wm.traces.common.
@@ -71,12 +75,14 @@ const Display = require('flicker').com.android.server.wm.traces.common.
// Common
const Size = require('flicker').com.android.server.wm.traces.common.Size;
const Buffer = require('flicker').com.android.server.wm.traces.common.Buffer;
const ActiveBuffer = require('flicker').com.android.server.wm.traces.common
.ActiveBuffer;
const Color3 = require('flicker').com.android.server.wm.traces.common.Color3;
const Color = require('flicker').com.android.server.wm.traces.common.Color;
const Point = require('flicker').com.android.server.wm.traces.common.Point;
const Rect = require('flicker').com.android.server.wm.traces.common.Rect;
const RectF = require('flicker').com.android.server.wm.traces.common.RectF;
const Region = require('flicker').com.android.server.wm.traces.common.Region;
const Region = require('flicker').com.android.server.wm.traces.common.region.Region;
//Tags
const Tag = require('flicker').com.android.server.wm.traces.common.tags.Tag;
@@ -91,13 +97,15 @@ const ErrorTrace = require('flicker').com.android.server.wm.traces.common.errors
// Service
const TaggingEngine = require('flicker').com.android.server.wm.traces.common.service.TaggingEngine;
const EMPTY_BUFFER = new Buffer(0, 0, 0, 0);
const EMPTY_BUFFER = new ActiveBuffer(0, 0, 0, 0);
const EMPTY_COLOR3 = new Color3(-1, -1, -1);
const EMPTY_COLOR = new Color(-1, -1, -1, 0);
const EMPTY_RECT = new Rect(0, 0, 0, 0);
const EMPTY_RECTF = new RectF(0, 0, 0, 0);
const EMPTY_POINT = new Point(0, 0);
const EMPTY_MATRIX = new Matrix(0, 0, 0, 0, 0, 0);
const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX);
const EMPTY_MATRIX22 = new Matrix22(0, 0, 0, 0, 0, 0);
const EMPTY_MATRIX33 = new Matrix33(0, 0, 0, 0, 0, 0);
const EMPTY_TRANSFORM = new Transform(0, EMPTY_MATRIX33);
function toSize(proto) {
if (proto == null) {
@@ -111,18 +119,31 @@ function toSize(proto) {
return EMPTY_BOUNDS;
}
function toBuffer(proto) {
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 Buffer(width, height, stride, format);
return new ActiveBuffer(width, height, stride, format);
}
return EMPTY_BUFFER;
}
function toColor3(proto) {
if (proto == null) {
return EMPTY_COLOR;
}
const r = proto.r ?? 0;
const g = proto.g ?? 0;
const b = proto.b ?? 0;
if (r || g || b) {
return new Color3(r, g, b);
}
return EMPTY_COLOR3;
}
function toColor(proto) {
if (proto == null) {
return EMPTY_COLOR;
@@ -206,16 +227,32 @@ function toTransform(proto) {
const ty = proto.ty ?? 0;
if (dsdx || dtdx || tx || dsdy || dtdy || ty) {
const matrix = new Matrix(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_MATRIX);
return new Transform(proto.type ?? 0, EMPTY_MATRIX33);
}
return EMPTY_TRANSFORM;
}
function toMatrix22(proto) {
if (proto == null) {
return EMPTY_MATRIX22;
}
const dsdx = proto.dsdx ?? 0;
const dtdx = proto.dtdx ?? 0;
const dsdy = proto.dsdy ?? 0;
const dtdy = proto.dtdy ?? 0;
if (dsdx || dtdx || dsdy || dtdy) {
return new Matrix22(dsdx, dtdx, dsdy, dtdy);
}
return EMPTY_MATRIX22;
}
export {
Activity,
Configuration,
@@ -235,12 +272,14 @@ export {
WindowManagerTrace,
WindowManagerState,
// SF
BaseLayerTraceEntry,
Layer,
LayerTraceEntry,
LayerTraceEntryBuilder,
LayersTrace,
Transform,
Matrix,
Matrix22,
Matrix33,
Display,
// Tags
Tag,
@@ -252,8 +291,9 @@ export {
ErrorTrace,
// Common
Size,
Buffer,
ActiveBuffer,
Color,
Color3,
Point,
Rect,
RectF,
@@ -261,11 +301,13 @@ export {
// Service
TaggingEngine,
toSize,
toBuffer,
toActiveBuffer,
toColor,
toColor3,
toPoint,
toRect,
toRectF,
toRegion,
toMatrix22,
toTransform,
};

View File

@@ -23,7 +23,8 @@ Error.fromProto = function (proto: any): Error {
proto.message,
proto.layerId,
proto.windowToken,
proto.taskId
proto.taskId,
proto.assertionName
);
return error;
}

View File

@@ -15,14 +15,14 @@
*/
import { Layer, Rect, toBuffer, toColor, toRect, toRectF, toRegion } from "../common"
import { Layer, Rect, toActiveBuffer, toColor, toRect, toRectF, toRegion } from "../common"
import { shortenName } from '../mixin'
import { RELATIVE_Z_CHIP, GPU_CHIP, HWC_CHIP } from '../treeview/Chips'
import Transform from './Transform'
Layer.fromProto = function (proto: any): Layer {
const visibleRegion = toRegion(proto.visibleRegion)
const activeBuffer = toBuffer(proto.activeBuffer)
const activeBuffer = toActiveBuffer(proto.activeBuffer)
const bounds = toRectF(proto.bounds)
const color = toColor(proto.color)
const screenBounds = toRectF(proto.screenBounds)
@@ -62,7 +62,8 @@ Layer.fromProto = function (proto: any): Layer {
proto.backgroundBlurRadius,
crop,
proto.isRelativeOf,
proto.zOrderRelativeOf
proto.zOrderRelativeOf,
proto.layerStack
);
addAttributes(entry, proto);

View File

@@ -72,7 +72,8 @@ function newDisplay(proto: any): Display {
proto.layerStack,
toSize(proto.size),
toRect(proto.layerStackSpaceRect),
toTransform(proto.transform)
toTransform(proto.transform),
proto.isVirtual
)
}

View File

@@ -0,0 +1,114 @@
/*
* 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 { BaseLayerTraceEntry } from "../common";
import LayerTraceEntry from "./LayerTraceEntry";
class LayerTraceEntryLazy extends BaseLayerTraceEntry {
private _isInitialized: boolean = false;
private _layersProto: any[];
private _displayProtos: any[];
timestamp: number;
timestampMs: string;
hwcBlob: string;
where: string;
private _lazyLayerTraceEntry: LayerTraceEntry;
constructor (layersProto: any[], displayProtos: any[],
timestamp: number, hwcBlob: string, where: string = '') {
super();
this._layersProto = layersProto;
this._displayProtos = displayProtos;
this.timestamp = timestamp;
this.timestampMs = timestamp.toString();
this.hwcBlob = hwcBlob;
this.where = where;
this.declareLazyProperties();
}
private initialize() {
if (this._isInitialized) return;
this._isInitialized = true;
this._lazyLayerTraceEntry = LayerTraceEntry.fromProto(
this._layersProto, this._displayProtos, this.timestamp,
this.hwcBlob, this.where);
this._layersProto = [];
this._displayProtos = [];
}
private declareLazyProperties() {
Object.defineProperty(this, 'kind', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.kind;
}});
Object.defineProperty(this, 'timestampMs', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.timestampMs;
}});
Object.defineProperty(this, 'rects', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.rects;
}});
Object.defineProperty(this, 'proto', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.proto;
}});
Object.defineProperty(this, 'shortName', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.shortName;
}});
Object.defineProperty(this, 'isVisible', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.isVisible;
}});
Object.defineProperty(this, 'flattenedLayers', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.flattenedLayers;
}});
Object.defineProperty(this, 'stableId', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.stableId;
}});
Object.defineProperty(this, 'visibleLayers', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.visibleLayers;
}});
Object.defineProperty(this, 'children', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.children;
}});
Object.defineProperty(this, 'displays', {configurable: true, enumerable: true, get: function () {
this.initialize();
return this._lazyLayerTraceEntry.displays;
}});
}
}
export default LayerTraceEntryLazy;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Transform, Matrix } from "../common"
import { Transform, Matrix33 } from "../common"
Transform.fromProto = function (transformProto, positionProto): Transform {
const entry = new Transform(
@@ -24,7 +24,7 @@ Transform.fromProto = function (transformProto, positionProto): Transform {
return entry
}
function getMatrix(transform, position): Matrix {
function getMatrix(transform, position): Matrix33 {
const x = position?.x ?? 0
const y = position?.y ?? 0
@@ -32,33 +32,33 @@ function getMatrix(transform, position): Matrix {
return getDefaultTransform(transform?.type, x, y)
}
return new Matrix(transform.dsdx, transform.dtdx, x, transform.dsdy, transform.dtdy, y)
return new Matrix33(transform.dsdx, transform.dtdx, x, transform.dsdy, transform.dtdy, y)
}
function getDefaultTransform(type, x, y): Matrix {
function getDefaultTransform(type, x, y): Matrix33 {
// IDENTITY
if (!type) {
return new Matrix(1, 0, x, 0, 1, y)
return new Matrix33(1, 0, x, 0, 1, y)
}
// ROT_270 = ROT_90|FLIP_H|FLIP_V
if (isFlagSet(type, ROT_90_VAL | FLIP_V_VAL | FLIP_H_VAL)) {
return new Matrix(0, -1, x, 1, 0, y)
return new Matrix33(0, -1, x, 1, 0, y)
}
// ROT_180 = FLIP_H|FLIP_V
if (isFlagSet(type, FLIP_V_VAL | FLIP_H_VAL)) {
return new Matrix(-1, 0, x, 0, -1, y)
return new Matrix33(-1, 0, x, 0, -1, y)
}
// ROT_90
if (isFlagSet(type, ROT_90_VAL)) {
return new Matrix(0, 1, x, -1, 0, y)
return new Matrix33(0, 1, x, -1, 0, y)
}
// IDENTITY
if (isFlagClear(type, SCALE_VAL | ROTATE_VAL)) {
return new Matrix(1, 0, x, 0, 1, y)
return new Matrix33(1, 0, x, 0, 1, y)
}
throw new Error(`Unknown transform type ${type}`)
@@ -87,4 +87,4 @@ const FLIP_V_VAL = 0x0200 // (1 << 1 << 8)
const ROT_90_VAL = 0x0400 // (1 << 2 << 8)
const ROT_INVALID_VAL = 0x8000 // (0x80 << 8)
export default Transform
export default Transform

View File

@@ -22,7 +22,7 @@ const transitionTypeMap = new Map([
['ROTATION', TransitionType.ROTATION],
['PIP_ENTER', TransitionType.PIP_ENTER],
['PIP_RESIZE', TransitionType.PIP_RESIZE],
['PIP_CLOSE', TransitionType.PIP_CLOSE],
['PIP_EXPAND', TransitionType.PIP_EXPAND],
['PIP_EXIT', TransitionType.PIP_EXIT],
['APP_LAUNCH', TransitionType.APP_LAUNCH],
['APP_CLOSE', TransitionType.APP_CLOSE],

Some files were not shown because too many files have changed in this diff Show More