Merge tm-dev-plus-aosp-without-vendor@8763363
Bug: 236760014 Merged-In: I794bcb9e9c922c642c3fbf0808fb60de4445ab65 Change-Id: Id292c36692b0020d772dc61b863f9cd3b39fe0c5
This commit is contained in:
@@ -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",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
75
ide/clion/frameworks/base/tools/aapt2/CMakeLists.txt
Normal file
75
ide/clion/frameworks/base/tools/aapt2/CMakeLists.txt
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
26
samples/ApiDemos/res/drawable/ic_call_end.xml
Normal file
26
samples/ApiDemos/res/drawable/ic_call_end.xml
Normal 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>
|
||||
63
samples/ApiDemos/res/layout/keep_clear_rects_activity.xml
Normal file
63
samples/ApiDemos/res/layout/keep_clear_rects_activity.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
25
samples/ApiDemos/res/layout/picture_in_picture_content.xml
Normal file
25
samples/ApiDemos/res/layout/picture_in_picture_content.xml
Normal 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" />
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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. -->
|
||||
|
||||
28
samples/ApiDemos/res/values/dimens.xml
Normal file
28
samples/ApiDemos/res/values/dimens.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
/>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
@@ -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>
|
||||
@@ -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="&"/>
|
||||
<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="""/>
|
||||
<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>
|
||||
@@ -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="<"/>
|
||||
<Key android:codes="62" android:keyLabel=">"/>
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
33
samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
Executable file
33
samples/SampleInputMethodAccessibilityService/AndroidManifest.xml
Executable 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>
|
||||
@@ -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" />
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -244,4 +244,4 @@ describe("DiffGenerator", () => {
|
||||
|
||||
checkDiffTreeWithNoModifiedCheck(oldTree, newTree, expectedDiffTree);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -155,4 +155,4 @@ describe("ObjectTransformer", () => {
|
||||
|
||||
expect(transformedObj).toEqual(expectedTransformedObj);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,4 +25,4 @@ describe("Proto Transformations", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)]);
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -55,4 +55,4 @@ function toPlainObject(theClass) {
|
||||
}
|
||||
}
|
||||
|
||||
export { Node, DiffNode, ObjNode, ObjDiffNode, toPlainObject };
|
||||
export { Node, DiffNode, ObjNode, ObjDiffNode, toPlainObject };
|
||||
@@ -38,4 +38,4 @@ export default {
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
class="inline-error"
|
||||
@click="setCurrentTimestamp(item.timestamp)"
|
||||
>
|
||||
{{item.message}}
|
||||
{{ `${item.assertionName} ${item.message}` }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
31
tools/winscope/src/Title.vue
Normal file
31
tools/winscope/src/Title.vue
Normal 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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
import { shortenName } from './flickerlib/mixin'
|
||||
|
||||
export default {
|
||||
name: 'transaction-entry',
|
||||
name: 'transaction-entry-legacy',
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -36,6 +36,8 @@ export default {
|
||||
name: 'videoview',
|
||||
props: ['file', 'height'],
|
||||
data() {
|
||||
// Record analytics event
|
||||
this.recordOpenTraceEvent("Video");
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import Arrow from './Arrow.vue';
|
||||
import {LocalStore} from '../../localstore.js';
|
||||
import LocalStore from '../../localstore.js';
|
||||
|
||||
var transitionCount = false;
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -43,4 +43,4 @@ export default class SurfaceFlinger extends DumpBase {
|
||||
);
|
||||
return new LayersTrace([entry], source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,4 +37,4 @@ export default class WindowManager extends DumpBase {
|
||||
const state = WindowManagerTrace.fromDump(proto);
|
||||
return new WindowManagerTrace([state], source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -23,7 +23,8 @@ Error.fromProto = function (proto: any): Error {
|
||||
proto.message,
|
||||
proto.layerId,
|
||||
proto.windowToken,
|
||||
proto.taskId
|
||||
proto.taskId,
|
||||
proto.assertionName
|
||||
);
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
114
tools/winscope/src/flickerlib/layers/LayerTraceEntryLazy.ts
Normal file
114
tools/winscope/src/flickerlib/layers/LayerTraceEntryLazy.ts
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user