summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--OWNERS7
-rw-r--r--PREUPLOAD.cfg (renamed from builtInServices/PREUPLOAD.cfg)1
-rw-r--r--builtInServices/Android.bp12
-rw-r--r--builtInServices/Android.mk20
-rw-r--r--builtInServices/api/module-lib-current.txt38
-rw-r--r--builtInServices/host_tests/Android.bp7
-rw-r--r--builtInServices/host_tests/AndroidTest.xml21
-rw-r--r--builtInServices/jni/Android.bp49
-rw-r--r--builtInServices/jni/com_android_internal_car_os_Util.cpp57
-rw-r--r--builtInServices/jni/onload.cpp39
-rw-r--r--builtInServices/prebuilts/Android.bp30
-rw-r--r--builtInServices/prebuilts/README.md2
-rw-r--r--builtInServices/prebuilts/mu_imms-prebuilt.jarbin0 -> 158987 bytes
-rw-r--r--builtInServices/src/com/android/annotation/AddedIn.java41
-rw-r--r--builtInServices/src/com/android/internal/car/CarActivityInterceptor.java90
-rw-r--r--builtInServices/src/com/android/internal/car/CarServiceHelperInterface.java52
-rw-r--r--builtInServices/src/com/android/internal/car/CarServiceHelperService.java220
-rw-r--r--builtInServices/src/com/android/internal/car/CarServiceHelperServiceUpdatable.java16
-rw-r--r--builtInServices/src/com/android/internal/car/os/Util.java40
-rw-r--r--builtInServices/src/com/android/server/wm/ActivityInterceptResultWrapper.java61
-rw-r--r--builtInServices/src/com/android/server/wm/ActivityInterceptorInfoWrapper.java85
-rw-r--r--builtInServices/src/com/android/server/wm/ActivityOptionsWrapper.java44
-rw-r--r--builtInServices/src/com/android/server/wm/ActivityRecordWrapper.java13
-rw-r--r--builtInServices/src/com/android/server/wm/CalculateParams.java14
-rw-r--r--builtInServices/src/com/android/server/wm/CarActivityInterceptorInterface.java58
-rw-r--r--builtInServices/src/com/android/server/wm/CarActivityInterceptorUpdatable.java47
-rw-r--r--builtInServices/src/com/android/server/wm/CarDisplayAreaPolicyProvider.java6
-rw-r--r--builtInServices/src/com/android/server/wm/CarLaunchParamsModifier.java37
-rw-r--r--builtInServices/src/com/android/server/wm/CarLaunchParamsModifierInterface.java41
-rw-r--r--builtInServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatable.java31
-rw-r--r--builtInServices/src/com/android/server/wm/LaunchParamsWrapper.java13
-rw-r--r--builtInServices/src/com/android/server/wm/RequestWrapper.java5
-rw-r--r--builtInServices/src/com/android/server/wm/TaskDisplayAreaWrapper.java8
-rw-r--r--builtInServices/src/com/android/server/wm/TaskWrapper.java19
-rw-r--r--builtInServices/src/com/android/server/wm/WindowLayoutWrapper.java4
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/AutofillController.java48
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarAutofillSuggestionsController.java238
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarDefaultImeVisibilityApplier.java178
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarImeVisibilityStateComputer.java738
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodBindingController.java540
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodManagerService.java6854
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodMenuController.java312
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/InputMethodManagerServiceProxy.java1133
-rw-r--r--builtInServices/src_imms/com/android/server/inputmethod/NullAutofillSuggestionsController.java43
-rw-r--r--builtInServices/tests/Android.bp48
-rw-r--r--builtInServices/tests/Android.mk47
-rw-r--r--builtInServices/tests/res/raw/CSHS_classes.txt16
-rw-r--r--builtInServices/tests/src/com/android/internal/car/CarServiceHelperServiceTest.java144
-rw-r--r--builtInServices/tests/src/com/android/server/wm/AnnotationTest.java45
-rw-r--r--tools/OWNERS1
-rwxr-xr-xtools/repohookScript/annotation_classlist_repohook.py111
-rw-r--r--updatableServices/Android.bp14
-rw-r--r--updatableServices/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImpl.java110
-rw-r--r--updatableServices/src/com/android/internal/car/updatable/CarServiceProxy.java60
-rw-r--r--updatableServices/src/com/android/server/wm/CarActivityInterceptorUpdatableImpl.java168
-rw-r--r--updatableServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatableImpl.java125
-rw-r--r--updatableServices/tests/Android.bp2
-rw-r--r--updatableServices/tests/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImplTest.java66
-rw-r--r--updatableServices/tests/src/com/android/internal/car/updatable/CarServiceProxyTest.java19
-rw-r--r--updatableServices/tests/src/com/android/server/wm/CarActivityInterceptorUpdatableTest.java270
-rw-r--r--updatableServices/tests/src/com/android/server/wm/CarLaunchParamsModifierUpdatableTest.java150
61 files changed, 12448 insertions, 260 deletions
diff --git a/OWNERS b/OWNERS
index 6010182..b6083dd 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,6 +1,3 @@
-keunyoung@google.com
-sgurun@google.com
-yizheng@google.com
-gurunagarajan@google.com
-felipeal@google.com
+include platform/packages/services/Car:/OWNERS
ycheo@google.com
+ericjeong@google.com
diff --git a/builtInServices/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 2811ea9..6f35a4e 100644
--- a/builtInServices/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,5 +1,6 @@
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+annotation_classlist_repohook = ${REPO_ROOT}/frameworks/opt/car/services/tools/repohookScript/annotation_classlist_repohook.py ${REPO_ROOT}
[Builtin Hooks]
commit_msg_changeid_field = true
diff --git a/builtInServices/Android.bp b/builtInServices/Android.bp
index eeda2da..e4f8a92 100644
--- a/builtInServices/Android.bp
+++ b/builtInServices/Android.bp
@@ -15,13 +15,21 @@ java_sdk_library {
],
static_libs: [
"android.car.watchdoglib",
- "android.automotive.watchdog.internal-V1-java",
+ "android.automotive.watchdog.internal-V3-java",
+ "mu_imms-prebuilt",
],
api_lint: {
enabled: true,
},
- min_sdk_version: "33",
+ stub_only_libs: [
+ "framework-annotations-lib",
+ ],
+
+ droiddoc_options: [
+ "--include-annotations --pass-through-annotation android.annotation.RequiresApi"
+ ],
+
apex_available: [
"//apex_available:platform",
"com.android.car.framework"
diff --git a/builtInServices/Android.mk b/builtInServices/Android.mk
new file mode 100644
index 0000000..46342c9
--- /dev/null
+++ b/builtInServices/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# Multi-User IMMS is guarded by `BUILD_AUTOMOTIVE_IMMS_PREBUILT`.
+# It should be only used in Android Auto Multi-User builds.
+# Changes in Android Core IME/IMMS/IMF AIDLs should not be blocked by this module.
+ifeq ($(BUILD_AUTOMOTIVE_IMMS_PREBUILT), true)
+
+LOCAL_MODULE := mu_imms
+LOCAL_SRC_FILES := $(call all-java-files-under, src_imms)
+LOCAL_JAVA_LIBRARIES := services.core.unboosted
+
+# This module should not be built as part of checkbuild
+LOCAL_DONT_CHECK_MODULE := true
+
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+endif # BUILD_AUTOMOTIVE_IMMS_PREBUILT
diff --git a/builtInServices/api/module-lib-current.txt b/builtInServices/api/module-lib-current.txt
index ddf15cf..ed6de32 100644
--- a/builtInServices/api/module-lib-current.txt
+++ b/builtInServices/api/module-lib-current.txt
@@ -4,11 +4,19 @@ package com.android.internal.car {
public interface CarServiceHelperInterface {
method @Nullable public android.os.UserHandle createUserEvenWhenDisallowed(@Nullable String, @NonNull String, int);
method @Nullable public java.io.File dumpServiceStacks();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int fetchAidlVhalPid();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getMainDisplayAssignedToUser(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getProcessGroup(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getUserAssignedToDisplay(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void setProcessGroup(int, int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void setProcessProfile(int, int, @NonNull String);
method public void setSafetyMode(boolean);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public boolean startUserInBackgroundVisibleOnDisplay(int, int);
}
public interface CarServiceHelperServiceUpdatable {
method public void dump(@NonNull java.io.PrintWriter, @Nullable String[]);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public com.android.server.wm.CarActivityInterceptorUpdatable getCarActivityInterceptorUpdatable();
method public com.android.server.wm.CarLaunchParamsModifierUpdatable getCarLaunchParamsModifierUpdatable();
method public void initBootUser();
method public void onFactoryReset(@NonNull java.util.function.BiConsumer<java.lang.Integer,android.os.Bundle>);
@@ -21,9 +29,25 @@ package com.android.internal.car {
package com.android.server.wm {
+ public final class ActivityInterceptResultWrapper {
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static com.android.server.wm.ActivityInterceptResultWrapper create(android.content.Intent, android.app.ActivityOptions);
+ }
+
+ public final class ActivityInterceptorInfoWrapper {
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public android.content.pm.ActivityInfo getActivityInfo();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public String getCallingPackage();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public com.android.server.wm.ActivityOptionsWrapper getCheckedOptions();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public android.content.Intent getIntent();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getUserId();
+ }
+
public final class ActivityOptionsWrapper {
+ method public static com.android.server.wm.ActivityOptionsWrapper create(android.app.ActivityOptions);
method public com.android.server.wm.TaskDisplayAreaWrapper getLaunchTaskDisplayArea();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getLaunchWindowingMode();
method public android.app.ActivityOptions getOptions();
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void setLaunchRootTask(android.os.IBinder);
+ field @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final int WINDOWING_MODE_UNDEFINED = 0; // 0x0
}
public final class ActivityRecordWrapper {
@@ -50,10 +74,22 @@ package com.android.server.wm {
method public boolean supportsMultiDisplay();
}
+ public interface CarActivityInterceptorInterface {
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getMainDisplayAssignedToUser(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getUserAssignedToDisplay(int);
+ }
+
+ public interface CarActivityInterceptorUpdatable {
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable public com.android.server.wm.ActivityInterceptResultWrapper onInterceptActivityLaunch(com.android.server.wm.ActivityInterceptorInfoWrapper);
+ }
+
public interface CarLaunchParamsModifierInterface {
method @Nullable public com.android.server.wm.TaskDisplayAreaWrapper findTaskDisplayArea(int, int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getCurrentAndTargetUserIds();
method @Nullable public com.android.server.wm.TaskDisplayAreaWrapper getDefaultTaskDisplayAreaOnDisplay(int);
method @NonNull public java.util.List<com.android.server.wm.TaskDisplayAreaWrapper> getFallbackDisplayAreasForActivity(@NonNull com.android.server.wm.ActivityRecordWrapper, @Nullable com.android.server.wm.RequestWrapper);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getMainDisplayAssignedToUser(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public int getUserAssignedToDisplay(int);
}
public interface CarLaunchParamsModifierUpdatable {
@@ -62,6 +98,7 @@ package com.android.server.wm {
method public void handleCurrentUserSwitching(int);
method public void handleUserStarting(int);
method public void handleUserStopped(int);
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public void handleUserVisibilityChanged(int, boolean);
}
public final class LaunchParamsWrapper {
@@ -84,6 +121,7 @@ package com.android.server.wm {
}
public final class TaskWrapper {
+ method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static com.android.server.wm.TaskWrapper createFromToken(@NonNull android.os.IBinder);
method public com.android.server.wm.TaskWrapper getRootTask();
method public com.android.server.wm.TaskDisplayAreaWrapper getTaskDisplayArea();
method public int getUserId();
diff --git a/builtInServices/host_tests/Android.bp b/builtInServices/host_tests/Android.bp
index 9d68572..7b62799 100644
--- a/builtInServices/host_tests/Android.bp
+++ b/builtInServices/host_tests/Android.bp
@@ -6,9 +6,14 @@ java_test_host {
name: "CarServiceCrashDumpTest",
srcs: ["src/**/CarServiceCrashDumpTest.java"],
libs: [
- "compatibility-host-util",
"junit",
"tradefed",
"truth-prebuilt",
],
+ static_libs: [
+ "compatibility-host-util",
+ ],
+ test_suites: [
+ "automotive-tests",
+ ],
}
diff --git a/builtInServices/host_tests/AndroidTest.xml b/builtInServices/host_tests/AndroidTest.xml
new file mode 100644
index 0000000..93c0ece
--- /dev/null
+++ b/builtInServices/host_tests/AndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for host side car service crash dump test">
+ <option name="test-suite-tag" value="automotive-tests" />
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="CarServiceCrashDumpTest.jar" />
+ </test>
+</configuration>
diff --git a/builtInServices/jni/Android.bp b/builtInServices/jni/Android.bp
new file mode 100644
index 0000000..4057b87
--- /dev/null
+++ b/builtInServices/jni/Android.bp
@@ -0,0 +1,49 @@
+// Copyright 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_shared {
+ name: "libcarservicehelperjni",
+ srcs: [
+ "onload.cpp",
+ "com_android_internal_car_os_Util.cpp",
+ ],
+
+ shared_libs: [
+ "libandroid",
+ "libandroid_runtime",
+ "libbase",
+ "liblog",
+ "libnativehelper",
+ "libprocessgroup",
+ "libutils",
+ ],
+
+ strip: {
+ keep_symbols: true,
+ },
+
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wunused",
+ "-Wunreachable-code",
+ "-fvisibility=hidden",
+ ],
+}
diff --git a/builtInServices/jni/com_android_internal_car_os_Util.cpp b/builtInServices/jni/com_android_internal_car_os_Util.cpp
new file mode 100644
index 0000000..43c485e
--- /dev/null
+++ b/builtInServices/jni/com_android_internal_car_os_Util.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "OsUtil"
+
+#include "core_jni_helpers.h"
+
+#include <processgroup/processgroup.h>
+#include <utils/Log.h>
+
+#include <nativehelper/JNIHelp.h>
+
+using namespace android;
+
+void com_android_internal_car_os_Util_setProcessProfile(JNIEnv* env, jobject /*clazz*/, jint pid,
+ jint uid, jstring profile) {
+ if (profile == nullptr) {
+ jniThrowNullPointerException(env, NULL);
+ return;
+ }
+
+ const char* profileStr = env->GetStringUTFChars(profile, nullptr);
+ if (profileStr == nullptr) {
+ jniThrowNullPointerException(env, NULL);
+ return;
+ }
+ bool success = SetProcessProfiles(uid, pid, {profileStr});
+ env->ReleaseStringUTFChars(profile, profileStr);
+
+ if (!success) {
+ jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException",
+ "setProcessProfile for pid %d, uid %d, profile %s failed", pid, uid, profileStr);
+ }
+}
+
+static const JNINativeMethod methods[] = {
+ {"setProcessProfile", "(IILjava/lang/String;)V",
+ (void*)com_android_internal_car_os_Util_setProcessProfile},
+};
+
+int register_com_android_internal_car_os_Util(JNIEnv* env)
+{
+ return RegisterMethodsOrDie(env, "com/android/internal/car/os/Util", methods, NELEM(methods));
+}
diff --git a/builtInServices/jni/onload.cpp b/builtInServices/jni/onload.cpp
new file mode 100644
index 0000000..d64f167
--- /dev/null
+++ b/builtInServices/jni/onload.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+#include "utils/Log.h"
+
+using namespace android;
+
+extern int register_com_android_internal_car_os_Util(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
+{
+ JNIEnv* env = NULL;
+ jint result = -1;
+
+ if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+ ALOGE("GetEnv failed!");
+ return result;
+ }
+ ALOG_ASSERT(env, "Could not retrieve the env!");
+
+ register_com_android_internal_car_os_Util(env);
+
+ return JNI_VERSION_1_4;
+}
diff --git a/builtInServices/prebuilts/Android.bp b/builtInServices/prebuilts/Android.bp
new file mode 100644
index 0000000..e31e675
--- /dev/null
+++ b/builtInServices/prebuilts/Android.bp
@@ -0,0 +1,30 @@
+// 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.
+
+// Multi-User / multi-Display IMMS prebuilt jar (allows concurrent IME sessions).
+// Instructions for building this jar from the repo root:
+// 1. Run: `export BUILD_AUTOMOTIVE_IMMS_PREBUILT=true && m mu_imms`
+// 2. Copy and rename the generated jar:
+// `cp out/target/common/obj/JAVA_LIBRARIES/mu_imms_intermediates/classes.jar \
+// frameworks/opt/car/services/builtInServices/prebuilts/mu_imms-prebuilt.jar`
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_import {
+ name: "mu_imms-prebuilt",
+ jars: ["mu_imms-prebuilt.jar"],
+ sdk_version: "current",
+ min_sdk_version: "33",
+}
diff --git a/builtInServices/prebuilts/README.md b/builtInServices/prebuilts/README.md
new file mode 100644
index 0000000..0710ea1
--- /dev/null
+++ b/builtInServices/prebuilts/README.md
@@ -0,0 +1,2 @@
+# How to build the mu_imms prebuilt jar.
+* Please check [this doc](http://go/aaos-mu-ime#building-and-deploying-mu-imms-prebuilt-jar).
diff --git a/builtInServices/prebuilts/mu_imms-prebuilt.jar b/builtInServices/prebuilts/mu_imms-prebuilt.jar
new file mode 100644
index 0000000..f2d0a50
--- /dev/null
+++ b/builtInServices/prebuilts/mu_imms-prebuilt.jar
Binary files differ
diff --git a/builtInServices/src/com/android/annotation/AddedIn.java b/builtInServices/src/com/android/annotation/AddedIn.java
new file mode 100644
index 0000000..6cfceb4
--- /dev/null
+++ b/builtInServices/src/com/android/annotation/AddedIn.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.annotation;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.car.builtin.annotation.PlatformVersion;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Defines in which version of platform this method / type / field was added.
+ *
+ * <p>Annotation should be used for APIs exposed to CarService module by {@code android.car.builtin}
+ *
+ * @hide
+ */
+@Retention(RUNTIME)
+@Target({ANNOTATION_TYPE, FIELD, TYPE, METHOD})
+public @interface AddedIn {
+ PlatformVersion value();
+}
diff --git a/builtInServices/src/com/android/internal/car/CarActivityInterceptor.java b/builtInServices/src/com/android/internal/car/CarActivityInterceptor.java
new file mode 100644
index 0000000..81c6d0b
--- /dev/null
+++ b/builtInServices/src/com/android/internal/car/CarActivityInterceptor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.car;
+
+import android.annotation.Nullable;
+import android.app.TaskInfo;
+import android.car.builtin.util.Slogf;
+import android.content.pm.ActivityInfo;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.ActivityInterceptResultWrapper;
+import com.android.server.wm.ActivityInterceptorCallback;
+import com.android.server.wm.ActivityInterceptorInfoWrapper;
+import com.android.server.wm.CarActivityInterceptorInterface;
+import com.android.server.wm.CarActivityInterceptorUpdatable;
+
+/**
+ * See {@link ActivityInterceptorCallback}.
+ *
+ * @hide
+ */
+public final class CarActivityInterceptor implements ActivityInterceptorCallback {
+ private static final String TAG = CarActivityInterceptor.class.getSimpleName();
+ private CarActivityInterceptorUpdatable mCarActivityInterceptorUpdatable;
+
+ public CarActivityInterceptor() {
+ mCarActivityInterceptorUpdatable = null;
+ }
+
+ /**
+ * Sets the given {@link CarActivityInterceptorUpdatable} which this internal class will
+ * communicate with.
+ */
+ public void setUpdatable(CarActivityInterceptorUpdatable carActivityInterceptorUpdatable) {
+ mCarActivityInterceptorUpdatable = carActivityInterceptorUpdatable;
+ }
+
+ @Nullable
+ @Override
+ public ActivityInterceptResult onInterceptActivityLaunch(ActivityInterceptorInfo info) {
+ if (mCarActivityInterceptorUpdatable == null) {
+ Slogf.w(TAG, "mCarActivityInterceptorUpdatable not set");
+ return null;
+ }
+ ActivityInterceptResultWrapper interceptResultWrapper = mCarActivityInterceptorUpdatable
+ .onInterceptActivityLaunch(ActivityInterceptorInfoWrapper.create(info));
+ if (interceptResultWrapper == null) {
+ return null;
+ }
+ return interceptResultWrapper.getInterceptResult();
+ }
+
+ @Override
+ public void onActivityLaunched(TaskInfo taskInfo, ActivityInfo activityInfo,
+ ActivityInterceptorInfo info) {
+ // do nothing
+ }
+
+ CarActivityInterceptorInterface getBuiltinInterface() {
+ return new CarActivityInterceptorInterface() {
+ @Override
+ public int getUserAssignedToDisplay(int displayId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int userId = umi.getUserAssignedToDisplay(displayId);
+ return userId;
+ }
+
+ @Override
+ public int getMainDisplayAssignedToUser(int userId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int displayId = umi.getMainDisplayAssignedToUser(userId);
+ return displayId;
+ }
+ };
+ }
+}
diff --git a/builtInServices/src/com/android/internal/car/CarServiceHelperInterface.java b/builtInServices/src/com/android/internal/car/CarServiceHelperInterface.java
index 362fd07..41a6986 100644
--- a/builtInServices/src/com/android/internal/car/CarServiceHelperInterface.java
+++ b/builtInServices/src/com/android/internal/car/CarServiceHelperInterface.java
@@ -17,9 +17,15 @@ package com.android.internal.car;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
import android.os.UserHandle;
+import com.android.annotation.AddedIn;
+
import java.io.File;
/**
@@ -32,18 +38,64 @@ public interface CarServiceHelperInterface {
/**
* Sets safety mode
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void setSafetyMode(boolean safe);
/**
* Creates user even when disallowed
*/
@Nullable
+ @AddedIn(PlatformVersion.TIRAMISU_0)
UserHandle createUserEvenWhenDisallowed(@Nullable String name, @NonNull String userType,
int flags);
/**
+ * Gets the main display assigned to the user.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int getMainDisplayAssignedToUser(@UserIdInt int userId);
+
+ /**
+ * Gets the full user (i.e., not profile) assigned to the display.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int getUserAssignedToDisplay(int displayId);
+
+ /**
* Dumps service stacks
*/
@Nullable
+ @AddedIn(PlatformVersion.TIRAMISU_0)
File dumpServiceStacks();
+
+ /** Check {@link android.os.Process#setProcessGroup(int, int)}. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ void setProcessGroup(int pid, int group);
+
+ /** Check {@link android.os.Process#getProcessGroup(int)}. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int getProcessGroup(int pid);
+
+ /** Check {@link ActivityManager#startUserInBackgroundVisibleOnDisplay(int, int)}. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ boolean startUserInBackgroundVisibleOnDisplay(@UserIdInt int userId, int displayId);
+
+ /** Check {@link android.os.Process#setProcessProfile(int, int, String)}. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ void setProcessProfile(int pid, int uid, @NonNull String profile);
+
+ /**
+ * Returns the PID for the AIDL VHAL service.
+ *
+ * On error, returns {@link com.android.car.internal.common.CommonConstants#INVALID_PID}.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int fetchAidlVhalPid();
}
diff --git a/builtInServices/src/com/android/internal/car/CarServiceHelperService.java b/builtInServices/src/com/android/internal/car/CarServiceHelperService.java
index 4e2333c..b516c1e 100644
--- a/builtInServices/src/com/android/internal/car/CarServiceHelperService.java
+++ b/builtInServices/src/com/android/internal/car/CarServiceHelperService.java
@@ -15,6 +15,9 @@
*/
package com.android.internal.car;
+import static com.android.car.internal.common.CommonConstants.INVALID_PID;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_CREATED;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_POST_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
@@ -22,10 +25,14 @@ import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVE
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_VISIBLE;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.server.wm.ActivityInterceptorCallback.PRODUCT_ORDERED_ID;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManager.DevicePolicyOperation;
import android.app.admin.DevicePolicyManager.OperationSafetyReason;
@@ -42,6 +49,8 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ServiceDebugInfo;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
@@ -51,19 +60,22 @@ import android.system.OsConstants;
import android.util.Dumpable;
import android.util.TimeUtils;
-import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
import com.android.car.internal.common.UserHelperLite;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.car.os.Util;
import com.android.internal.os.IResultReceiver;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.Watchdog;
-import com.android.server.am.ActivityManagerService;
+import com.android.server.am.StackTracesDumpHelper;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerInternal.UserLifecycleListener;
+import com.android.server.pm.UserManagerInternal.UserVisibilityListener;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.CarActivityInterceptorInterface;
import com.android.server.wm.CarLaunchParamsModifier;
import com.android.server.wm.CarLaunchParamsModifierInterface;
@@ -81,6 +93,7 @@ import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
@@ -102,12 +115,26 @@ public class CarServiceHelperService extends SystemService
private static final boolean DBG = true;
private static final boolean VERBOSE = true;
- private static final List<String> CAR_HAL_INTERFACES_OF_INTEREST = Arrays.asList(
- "android.hardware.automotive.vehicle@2.0::IVehicle",
+ private static final List<String> CAR_HIDL_INTERFACES_OF_INTEREST = Arrays.asList(
"android.hardware.automotive.audiocontrol@1.0::IAudioControl",
- "android.hardware.automotive.audiocontrol@2.0::IAudioControl"
+ "android.hardware.automotive.audiocontrol@2.0::IAudioControl",
+ "android.hardware.automotive.can@1.0::ICanBus",
+ "android.hardware.automotive.can@1.0::ICanController",
+ "android.hardware.automotive.evs@1.0::IEvsEnumerator",
+ "android.hardware.automotive.sv@1.0::ISurroundViewService",
+ "android.hardware.automotive.vehicle@2.0::IVehicle"
);
+ private static final String[] CAR_AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
+ "android.hardware.automotive.audiocontrol.IAudioControl/",
+ "android.hardware.automotive.can.ICanController/",
+ "android.hardware.automotive.evs.IEvsEnumerator/",
+ "android.hardware.automotive.ivn.IIvnAndroidDevice/",
+ "android.hardware.automotive.occupant_awareness.IOccupantAwareness/",
+ "android.hardware.automotive.remoteaccess.IRemoteAccess/",
+ "android.hardware.automotive.vehicle.IVehicle/",
+ };
+
// Message ID representing post-processing of process dumping.
private static final int WHAT_POST_PROCESS_DUMPING = 1;
// Message ID representing process killing.
@@ -118,6 +145,13 @@ public class CarServiceHelperService extends SystemService
private static final String PROC_PID_STAT_PATTERN =
"(?<pid>[0-9]*)\\s\\((?<name>\\S+)\\)\\s\\S\\s(?:-?[0-9]*\\s){18}"
+ "(?<startClockTicks>[0-9]*)\\s(?:-?[0-9]*\\s)*-?[0-9]*";
+ private static final String AIDL_VHAL_INTERFACE_PREFIX =
+ "android.hardware.automotive.vehicle.IVehicle/";
+
+ static {
+ // Load this JNI before other classes are loaded.
+ System.loadLibrary("carservicehelperjni");
+ }
private final Context mContext;
private final Object mLock = new Object();
@@ -125,6 +159,7 @@ public class CarServiceHelperService extends SystemService
private boolean mSystemBootCompleted;
private final CarLaunchParamsModifier mCarLaunchParamsModifier;
+ private final CarActivityInterceptor mCarActivityInterceptor;
private final Handler mHandler;
private final HandlerThread mHandlerThread = new HandlerThread("CarServiceHelperService");
@@ -173,15 +208,18 @@ public class CarServiceHelperService extends SystemService
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mCarLaunchParamsModifier = carLaunchParamsModifier;
+ mCarActivityInterceptor = new CarActivityInterceptor();
mCarWatchdogDaemonHelper = carWatchdogDaemonHelper;
try {
if (carServiceHelperServiceUpdatable == null) {
mCarServiceHelperServiceUpdatable = (CarServiceHelperServiceUpdatable) Class
.forName(CSHS_UPDATABLE_CLASSNAME_STRING)
.getConstructor(Context.class, CarServiceHelperInterface.class,
- CarLaunchParamsModifierInterface.class)
+ CarLaunchParamsModifierInterface.class,
+ CarActivityInterceptorInterface.class)
.newInstance(mContext, this,
- mCarLaunchParamsModifier.getBuiltinInterface());
+ mCarLaunchParamsModifier.getBuiltinInterface(),
+ mCarActivityInterceptor.getBuiltinInterface());
Slogf.d(TAG, "CarServiceHelperServiceUpdatable created via reflection.");
} else {
mCarServiceHelperServiceUpdatable = carServiceHelperServiceUpdatable;
@@ -198,6 +236,8 @@ public class CarServiceHelperService extends SystemService
}
mCarLaunchParamsModifier.setUpdatable(
mCarServiceHelperServiceUpdatable.getCarLaunchParamsModifierUpdatable());
+ mCarActivityInterceptor.setUpdatable(mCarServiceHelperServiceUpdatable
+ .getCarActivityInterceptorUpdatable());
UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
if (umi != null) {
@@ -205,13 +245,31 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserCreated(UserInfo user, Object token) {
if (DBG) Slogf.d(TAG, "onUserCreated(): %s", user.toFullString());
+ mCarServiceHelperServiceUpdatable.sendUserLifecycleEvent(
+ USER_LIFECYCLE_EVENT_TYPE_CREATED, /* userFrom= */ null,
+ user.getUserHandle());
}
+
@Override
public void onUserRemoved(UserInfo user) {
if (DBG) Slogf.d(TAG, "onUserRemoved(): $s", user.toFullString());
mCarServiceHelperServiceUpdatable.onUserRemoved(user.getUserHandle());
}
});
+ umi.addUserVisibilityListener(new UserVisibilityListener() {
+ @Override
+ public void onUserVisibilityChanged(@UserIdInt int userId, boolean visible) {
+ if (DBG) {
+ Slogf.d(TAG, "onUserVisibilityChanged(%d, %b)", userId, visible);
+ }
+ int eventType = visible
+ ? USER_LIFECYCLE_EVENT_TYPE_VISIBLE
+ : USER_LIFECYCLE_EVENT_TYPE_INVISIBLE;
+ mCarServiceHelperServiceUpdatable.sendUserLifecycleEvent(eventType,
+ /* userFrom= */ null, UserHandle.of(userId));
+ mCarLaunchParamsModifier.handleUserVisibilityChanged(userId, visible);
+ }
+ });
} else {
Slogf.e(TAG, "UserManagerInternal not available - should only happen on unit tests");
}
@@ -242,6 +300,11 @@ public class CarServiceHelperService extends SystemService
} catch (RemoteException | RuntimeException e) {
Slogf.w(TAG, "Failed to notify boot phase change: %s", e);
}
+ ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService(
+ ActivityTaskManagerInternal.class);
+ activityTaskManagerInternal.registerActivityStartInterceptor(
+ PRODUCT_ORDERED_ID,
+ mCarActivityInterceptor);
t.traceEnd();
}
}
@@ -302,7 +365,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserUnlocking(@NonNull TargetUser user) {
- if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_UNLOCKING)) return;
EventLogHelper.writeCarHelperUserUnlocking(user.getUserIdentifier());
if (DBG) Slogf.d(TAG, "onUserUnlocking(%s)", user);
@@ -313,7 +375,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserUnlocked(@NonNull TargetUser user) {
- if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_UNLOCKED)) return;
int userId = user.getUserIdentifier();
EventLogHelper.writeCarHelperUserUnlocked(userId);
if (DBG) Slogf.d(TAG, "onUserUnlocked(%s)", user);
@@ -330,7 +391,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserStarting(@NonNull TargetUser user) {
- if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STARTING)) return;
EventLogHelper.writeCarHelperUserStarting(user.getUserIdentifier());
if (DBG) Slogf.d(TAG, "onUserStarting(%s)", user);
@@ -342,7 +402,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserStopping(@NonNull TargetUser user) {
- if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STOPPING)) return;
EventLogHelper.writeCarHelperUserStopping(user.getUserIdentifier());
if (DBG) Slogf.d(TAG, "onUserStopping(%s)", user);
@@ -354,7 +413,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserStopped(@NonNull TargetUser user) {
- if (isPreCreated(user, USER_LIFECYCLE_EVENT_TYPE_STOPPED)) return;
EventLogHelper.writeCarHelperUserStopped(user.getUserIdentifier());
if (DBG) Slogf.d(TAG, "onUserStopped(%s)", user);
@@ -364,7 +422,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
- if (isPreCreated(to, USER_LIFECYCLE_EVENT_TYPE_SWITCHING)) return;
EventLogHelper.writeCarHelperUserSwitching(
from == null ? UserHandle.USER_NULL : from.getUserIdentifier(),
to.getUserIdentifier());
@@ -379,14 +436,6 @@ public class CarServiceHelperService extends SystemService
@Override
public void onUserCompletedEvent(TargetUser user, UserCompletedEventType eventType) {
- if (user.isPreCreated()) {
- if (DBG) {
- Slogf.d(TAG, "Ignoring USER_COMPLETED event %s for pre-created user %s",
- eventType, user);
- }
- return;
- }
-
UserHandle handle = user.getUserHandle();
if (eventType.includesOnUserUnlocked()) {
mCarServiceHelperServiceUpdatable.sendUserLifecycleEvent(
@@ -422,13 +471,13 @@ public class CarServiceHelperService extends SystemService
}
}
- private boolean isPreCreated(@NonNull TargetUser user, @UserLifecycleEventType int eventType) {
- if (!user.isPreCreated()) return false;
-
- if (DBG) {
- Slogf.d(TAG, "Ignoring event of type %d for pre-created user %s", eventType, user);
+ @Override
+ public boolean isUserSupported(TargetUser user) {
+ boolean isPreCreated = user.isPreCreated();
+ if (isPreCreated && DBG) {
+ Slogf.d(TAG, "Not supporting Pre-created user %s", user);
}
- return true;
+ return !isPreCreated;
}
private TimingsTraceAndSlog newTimingsTraceAndSlog() {
@@ -444,45 +493,67 @@ public class CarServiceHelperService extends SystemService
// Adapted from frameworks/base/services/core/java/com/android/server/Watchdog.java
// TODO(b/131861630) use implementation common with Watchdog.java
- //
- private static ArrayList<Integer> getInterestingHalPids() {
+ private static void addInterestingHidlPids(HashSet<Integer> pids) {
try {
IServiceManager serviceManager = IServiceManager.getService();
ArrayList<IServiceManager.InstanceDebugInfo> dump =
serviceManager.debugDump();
- HashSet<Integer> pids = new HashSet<>();
for (IServiceManager.InstanceDebugInfo info : dump) {
if (info.pid == IServiceManager.PidConstant.NO_PID) {
continue;
}
if (Watchdog.HAL_INTERFACES_OF_INTEREST.contains(info.interfaceName) ||
- CAR_HAL_INTERFACES_OF_INTEREST.contains(info.interfaceName)) {
+ CAR_HIDL_INTERFACES_OF_INTEREST.contains(info.interfaceName)) {
pids.add(info.pid);
}
}
-
- return new ArrayList<Integer>(pids);
} catch (RemoteException e) {
- return new ArrayList<Integer>();
+ Slogf.w(TAG, "Remote exception while querying HIDL service manager", e);
+ }
+ }
+
+ // Adapted from frameworks/base/services/core/java/com/android/server/Watchdog.java
+ // TODO(b/131861630) use implementation common with Watchdog.java
+ private static void addInterestingAidlPids(HashSet<Integer> pids) {
+ ServiceDebugInfo[] infos = ServiceManager.getServiceDebugInfo();
+ if (infos == null) return;
+
+ for (ServiceDebugInfo info : infos) {
+ if (matchesInterestingAidlInterfacePrefixes(
+ Watchdog.AIDL_INTERFACE_PREFIXES_OF_INTEREST, info.name)
+ || matchesInterestingAidlInterfacePrefixes(
+ CAR_AIDL_INTERFACE_PREFIXES_OF_INTEREST, info.name)) {
+ pids.add(info.debugPid);
+ }
}
}
+ private static boolean matchesInterestingAidlInterfacePrefixes(String[] interfacePrefixes,
+ String interfaceName) {
+ for (String prefix : interfacePrefixes) {
+ if (interfaceName.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
// Adapted from frameworks/base/services/core/java/com/android/server/Watchdog.java
// TODO(b/131861630) use implementation common with Watchdog.java
- //
private static ArrayList<Integer> getInterestingNativePids() {
- ArrayList<Integer> pids = getInterestingHalPids();
+ HashSet<Integer> pids = new HashSet<Integer>();
+ addInterestingHidlPids(pids);
+ addInterestingAidlPids(pids);
int[] nativePids = Process.getPidsForCommands(Watchdog.NATIVE_STACKS_OF_INTEREST);
if (nativePids != null) {
- pids.ensureCapacity(pids.size() + nativePids.length);
for (int i : nativePids) {
pids.add(i);
}
}
- return pids;
+ return new ArrayList<Integer>(pids);
}
/**
@@ -495,8 +566,51 @@ public class CarServiceHelperService extends SystemService
ArrayList<Integer> pids = new ArrayList<>();
pids.add(Process.myPid());
- return ActivityManagerService.dumpStackTraces(
- pids, null, null, getInterestingNativePids(), null);
+ // Use the long version used by Watchdog since the short version is removed by the compiler.
+ return StackTracesDumpHelper.dumpStackTraces(
+ pids, /* processCpuTracker= */ null, /* lastPids= */ null,
+ CompletableFuture.completedFuture(getInterestingNativePids()),
+ /* logExceptionCreatingFile= */ null, /* subject= */ null,
+ /* criticalEventSection= */ null, Runnable::run, /* latencyTracker= */ null);
+ }
+
+ @Override
+ public void setProcessGroup(int pid, int group) {
+ Process.setProcessGroup(pid, group);
+ }
+
+ @Override
+ public int getProcessGroup(int pid) {
+ return Process.getProcessGroup(pid);
+ }
+
+ @Override
+ public boolean startUserInBackgroundVisibleOnDisplay(int userId, int displayId) {
+ ActivityManager am = mContext.getSystemService(ActivityManager.class);
+ return am.startUserInBackgroundVisibleOnDisplay(userId, displayId);
+ }
+
+ @Override
+ public void setProcessProfile(int pid, int uid, @NonNull String profile) {
+ Util.setProcessProfile(pid, uid, profile);
+ }
+
+ @Override
+ public int fetchAidlVhalPid() {
+ ServiceDebugInfo[] infos = ServiceManager.getServiceDebugInfo();
+ if (infos == null) {
+ Slogf.w(TAG, "Service debug info returned by the service manager is null");
+ return INVALID_PID;
+ }
+
+ for (ServiceDebugInfo info : infos) {
+ if (info.name.startsWith(AIDL_VHAL_INTERFACE_PREFIX)) {
+ return info.debugPid;
+ }
+ }
+ Slogf.w(TAG, "Service manager doesn't have the AIDL VHAL service instance for interface"
+ + " prefix %s", AIDL_VHAL_INTERFACE_PREFIX);
+ return INVALID_PID;
}
private void handleClientsNotResponding(@NonNull List<ProcessIdentifier> processIdentifiers) {
@@ -592,6 +706,26 @@ public class CarServiceHelperService extends SystemService
}
}
+ @Override
+ public int getMainDisplayAssignedToUser(int userId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int displayId = umi.getMainDisplayAssignedToUser(userId);
+ if (DBG) {
+ Slogf.d(TAG, "getMainDisplayAssignedToUser(%d): %d", userId, displayId);
+ }
+ return displayId;
+ }
+
+ @Override
+ public int getUserAssignedToDisplay(int displayId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int userId = umi.getUserAssignedToDisplay(displayId);
+ if (DBG) {
+ Slogf.d(TAG, "getUserAssignedToDisplay(%d): %d", displayId, userId);
+ }
+ return userId;
+ }
+
private class ICarWatchdogMonitorImpl extends ICarWatchdogMonitor.Stub {
private final WeakReference<CarServiceHelperService> mService;
@@ -679,7 +813,11 @@ public class CarServiceHelperService extends SystemService
}
nativePids.addAll(getInterestingNativePids());
long startDumpTime = SystemClock.uptimeMillis();
- ActivityManagerService.dumpStackTraces(javaPids, null, null, nativePids, null);
+ StackTracesDumpHelper.dumpStackTraces(
+ /* firstPids= */ javaPids, /* processCpuTracker= */ null, /* lastPids= */ null,
+ /* nativePids= */ CompletableFuture.completedFuture(nativePids),
+ /* logExceptionCreatingFile= */ null,
+ /* auxiliaryTaskExecutor= */ Runnable::run, /* latencyTracker= */ null);
long dumpTime = SystemClock.uptimeMillis() - startDumpTime;
if (DBG) {
Slogf.d(TAG, "Dumping process took %dms", dumpTime);
diff --git a/builtInServices/src/com/android/internal/car/CarServiceHelperServiceUpdatable.java b/builtInServices/src/com/android/internal/car/CarServiceHelperServiceUpdatable.java
index 175da48..3fb8a52 100644
--- a/builtInServices/src/com/android/internal/car/CarServiceHelperServiceUpdatable.java
+++ b/builtInServices/src/com/android/internal/car/CarServiceHelperServiceUpdatable.java
@@ -17,10 +17,15 @@ package com.android.internal.car;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
import android.os.Bundle;
import android.os.UserHandle;
+import com.android.annotation.AddedIn;
+import com.android.server.wm.CarActivityInterceptorUpdatable;
import com.android.server.wm.CarLaunchParamsModifierUpdatable;
import java.io.PrintWriter;
@@ -35,18 +40,29 @@ import java.util.function.BiConsumer;
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public interface CarServiceHelperServiceUpdatable {
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void onUserRemoved(@NonNull UserHandle userHandle);
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void onStart();
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void dump(@NonNull PrintWriter pw, @Nullable String[] args);
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void sendUserLifecycleEvent(int eventType, @Nullable UserHandle userFrom,
@NonNull UserHandle userTo);
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void onFactoryReset(@NonNull BiConsumer<Integer, Bundle> processFactoryReset);
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void initBootUser();
+ @AddedIn(PlatformVersion.TIRAMISU_0)
CarLaunchParamsModifierUpdatable getCarLaunchParamsModifierUpdatable();
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ CarActivityInterceptorUpdatable getCarActivityInterceptorUpdatable();
}
diff --git a/builtInServices/src/com/android/internal/car/os/Util.java b/builtInServices/src/com/android/internal/car/os/Util.java
new file mode 100644
index 0000000..707ed5b
--- /dev/null
+++ b/builtInServices/src/com/android/internal/car/os/Util.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.car.os;
+
+import android.annotation.NonNull;
+
+/**
+ * Generic OS level utility which can also have JNI support.
+ * @hide
+ */
+public class Util {
+
+ /**
+ * Assigns the given process to the specified process profile.
+ *
+ * <p>It will throw {@link IllegalArgumentException} for any failure.
+ *
+ * @param pid PID of the target process.
+ * @param uid UID of the target process.
+ * @param profile Process profile to set.
+ *
+ * @hide
+ */
+ public static native void setProcessProfile(int pid, int uid, @NonNull String profile);
+
+}
diff --git a/builtInServices/src/com/android/server/wm/ActivityInterceptResultWrapper.java b/builtInServices/src/com/android/server/wm/ActivityInterceptResultWrapper.java
new file mode 100644
index 0000000..5c9dea5
--- /dev/null
+++ b/builtInServices/src/com/android/server/wm/ActivityInterceptResultWrapper.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.app.ActivityOptions;
+import android.car.builtin.annotation.PlatformVersion;
+import android.content.Intent;
+import android.os.Build;
+
+import com.android.annotation.AddedIn;
+
+/**
+ * A wrapper over {@link com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptResult}.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ActivityInterceptResultWrapper {
+ private final ActivityInterceptorCallback.ActivityInterceptResult mActivityInterceptorInfo;
+
+ private ActivityInterceptResultWrapper(
+ ActivityInterceptorCallback.ActivityInterceptResult interceptorInfo) {
+ mActivityInterceptorInfo = interceptorInfo;
+ }
+
+ /**
+ * Creates an instance of {@link ActivityInterceptResultWrapper}.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public static ActivityInterceptResultWrapper create(Intent intent,
+ ActivityOptions activityOptions) {
+ return new ActivityInterceptResultWrapper(
+ new ActivityInterceptorCallback.ActivityInterceptResult(intent, activityOptions));
+ }
+
+ /**
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public ActivityInterceptorCallback.ActivityInterceptResult getInterceptResult() {
+ return mActivityInterceptorInfo;
+ }
+}
diff --git a/builtInServices/src/com/android/server/wm/ActivityInterceptorInfoWrapper.java b/builtInServices/src/com/android/server/wm/ActivityInterceptorInfoWrapper.java
new file mode 100644
index 0000000..eb2ebf1
--- /dev/null
+++ b/builtInServices/src/com/android/server/wm/ActivityInterceptorInfoWrapper.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Build;
+
+import com.android.annotation.AddedIn;
+
+/**
+ * A wrapper over {@link com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo}.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class ActivityInterceptorInfoWrapper {
+ private final ActivityInterceptorCallback.ActivityInterceptorInfo mActivityInterceptorInfo;
+
+ private ActivityInterceptorInfoWrapper(
+ ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo) {
+ mActivityInterceptorInfo = interceptorInfo;
+ }
+
+ /**
+ * Creates an instance of {@link ActivityInterceptorInfoWrapper}.
+ *
+ * @param interceptorInfo the original interceptorInfo that needs to be wrapped.
+ * @hide
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public static ActivityInterceptorInfoWrapper create(
+ ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo) {
+ return new ActivityInterceptorInfoWrapper(interceptorInfo);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public Intent getIntent() {
+ return mActivityInterceptorInfo.getIntent();
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public ActivityInfo getActivityInfo() {
+ return mActivityInterceptorInfo.getActivityInfo();
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public ActivityOptionsWrapper getCheckedOptions() {
+ return ActivityOptionsWrapper.create(mActivityInterceptorInfo.getCheckedOptions());
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public String getCallingPackage() {
+ return mActivityInterceptorInfo.getCallingPackage();
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public int getUserId() {
+ return mActivityInterceptorInfo.getUserId();
+ }
+}
diff --git a/builtInServices/src/com/android/server/wm/ActivityOptionsWrapper.java b/builtInServices/src/com/android/server/wm/ActivityOptionsWrapper.java
index f9dc751..1c65b68 100644
--- a/builtInServices/src/com/android/server/wm/ActivityOptionsWrapper.java
+++ b/builtInServices/src/com/android/server/wm/ActivityOptionsWrapper.java
@@ -16,10 +16,16 @@
package com.android.server.wm;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
import android.app.ActivityOptions;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
+import android.os.IBinder;
import android.window.WindowContainerToken;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of {@link ActivityOptions}.
* @hide
@@ -28,11 +34,19 @@ import android.window.WindowContainerToken;
public final class ActivityOptionsWrapper {
private final ActivityOptions mOptions;
+ /** See {@link android.app.WindowConfiguration#WINDOWING_MODE_UNDEFINED}. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public static final int WINDOWING_MODE_UNDEFINED = 0;
+
private ActivityOptionsWrapper(ActivityOptions options) {
mOptions = options;
}
- /** @hide */
+ /**
+ * Creates a new instance of {@link ActivityOptionsWrapper}.
+ */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static ActivityOptionsWrapper create(ActivityOptions options) {
if (options == null) return null;
return new ActivityOptionsWrapper(options);
@@ -42,13 +56,24 @@ public final class ActivityOptionsWrapper {
* Gets the underlying {@link ActivityOptions} that is wrapped by this instance.
*/
// Exposed the original object in order to allow to use the public accessors.
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityOptions getOptions() {
return mOptions;
}
/**
+ * Gets the windowing mode to launch the Activity into
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public int getLaunchWindowingMode() {
+ return mOptions.getLaunchWindowingMode();
+ }
+
+ /**
* Gets {@link TaskDisplayAreaWrapper} to launch the Activity into
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayAreaWrapper getLaunchTaskDisplayArea() {
WindowContainerToken daToken = mOptions.getLaunchTaskDisplayArea();
if (daToken == null) return null;
@@ -58,6 +83,21 @@ public final class ActivityOptionsWrapper {
@Override
public String toString() {
- return mOptions.toString();
+ StringBuilder sb = new StringBuilder(mOptions.toString());
+ sb.append(" ,mLaunchDisplayId=");
+ sb.append(mOptions.getLaunchDisplayId());
+ return sb.toString();
+ }
+
+ /**
+ * Sets the given {@code windowContainerToken} as the launch root task. See
+ * {@link ActivityOptions#setLaunchRootTask(WindowContainerToken)} for more info.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ public void setLaunchRootTask(IBinder windowContainerToken) {
+ WindowContainerToken launchRootTaskToken = WindowContainer.fromBinder(windowContainerToken)
+ .mRemoteToken.toWindowContainerToken();
+ mOptions.setLaunchRootTask(launchRootTaskToken);
}
}
diff --git a/builtInServices/src/com/android/server/wm/ActivityRecordWrapper.java b/builtInServices/src/com/android/server/wm/ActivityRecordWrapper.java
index bd15a54..9bf6b30 100644
--- a/builtInServices/src/com/android/server/wm/ActivityRecordWrapper.java
+++ b/builtInServices/src/com/android/server/wm/ActivityRecordWrapper.java
@@ -18,9 +18,12 @@ package com.android.server.wm;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of {@link ActivityRecord}.
* @hide
@@ -34,12 +37,14 @@ public final class ActivityRecordWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static ActivityRecordWrapper create(@Nullable ActivityRecord activityRecord) {
if (activityRecord == null) return null;
return new ActivityRecordWrapper(activityRecord);
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityRecord getActivityRecord() {
return mActivityRecord;
}
@@ -47,6 +52,7 @@ public final class ActivityRecordWrapper {
/**
* Gets which user this Activity is running for.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public int getUserId() {
return mActivityRecord.mUserId;
}
@@ -54,6 +60,7 @@ public final class ActivityRecordWrapper {
/**
* Gets the actual {@link ComponentName} of this Activity.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ComponentName getComponentName() {
if (mActivityRecord.info == null) return null;
return mActivityRecord.info.getComponentName();
@@ -62,6 +69,7 @@ public final class ActivityRecordWrapper {
/**
* Gets the {@link TaskDisplayAreaWrapper} where this is located.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayAreaWrapper getDisplayArea() {
return TaskDisplayAreaWrapper.create(mActivityRecord.getDisplayArea());
}
@@ -69,6 +77,7 @@ public final class ActivityRecordWrapper {
/**
* Returns whether this Activity is not displayed.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public boolean isNoDisplay() {
return mActivityRecord.noDisplay;
}
@@ -76,6 +85,7 @@ public final class ActivityRecordWrapper {
/**
* Gets {@link TaskDisplayAreaWrapper} where the handover Activity is supposed to launch
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayAreaWrapper getHandoverTaskDisplayArea() {
return TaskDisplayAreaWrapper.create(mActivityRecord.mHandoverTaskDisplayArea);
}
@@ -83,6 +93,7 @@ public final class ActivityRecordWrapper {
/**
* Gets {@code displayId} where the handover Activity is supposed to launch
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public int getHandoverLaunchDisplayId() {
return mActivityRecord.mHandoverLaunchDisplayId;
}
@@ -90,6 +101,7 @@ public final class ActivityRecordWrapper {
/**
* Returns whether this Activity allows to be embedded in the other Activity.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public boolean allowingEmbedded() {
if (mActivityRecord.info == null) return false;
return (mActivityRecord.info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
@@ -98,6 +110,7 @@ public final class ActivityRecordWrapper {
/**
* Returns whether the display where this Activity is located is trusted.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public boolean isDisplayTrusted() {
return mActivityRecord.getDisplayContent().isTrusted();
}
diff --git a/builtInServices/src/com/android/server/wm/CalculateParams.java b/builtInServices/src/com/android/server/wm/CalculateParams.java
index 8b4faff..0973882 100644
--- a/builtInServices/src/com/android/server/wm/CalculateParams.java
+++ b/builtInServices/src/com/android/server/wm/CalculateParams.java
@@ -18,9 +18,12 @@ package com.android.server.wm;
import android.annotation.SystemApi;
import android.app.ActivityOptions;
+import android.car.builtin.annotation.PlatformVersion;
import android.content.pm.ActivityInfo;
import android.view.WindowLayout;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of the parameters of {@code LaunchParamsController.LaunchParamsModifier.onCalculate()}
* @hide
@@ -41,6 +44,7 @@ public final class CalculateParams {
private CalculateParams() {}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static CalculateParams create(Task task, ActivityInfo.WindowLayout layout,
ActivityRecord actvity, ActivityRecord source,
ActivityOptions options, ActivityStarter.Request request, int phase,
@@ -64,6 +68,7 @@ public final class CalculateParams {
/**
* Gets the {@link TaskWrapper} currently being positioned.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskWrapper getTask() {
return mTask;
}
@@ -71,6 +76,7 @@ public final class CalculateParams {
/**
* Gets the specified {@link WindowLayoutWrapper}.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public WindowLayoutWrapper getWindowLayout() {
return mLayout;
}
@@ -78,6 +84,7 @@ public final class CalculateParams {
/**
* Gets the {@link ActivityRecordWrapper} currently being positioned.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityRecordWrapper getActivity() {
return mActivity;
}
@@ -85,6 +92,7 @@ public final class CalculateParams {
/**
* Gets the {@link ActivityRecordWrapper} from which activity was started from.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityRecordWrapper getSource() {
return mSource;
}
@@ -92,6 +100,7 @@ public final class CalculateParams {
/**
* Gets the {@link ActivityOptionsWrapper} specified for the activity.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityOptionsWrapper getOptions() {
return mOptions;
}
@@ -99,6 +108,7 @@ public final class CalculateParams {
/**
* Gets the optional {@link RequestWrapper} from the activity starter.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public RequestWrapper getRequest() {
return mRequest;
}
@@ -107,6 +117,7 @@ public final class CalculateParams {
* Gets the {@link LaunchParamsController.LaunchParamsModifier.Phase} that the resolution should
* finish.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public int getPhase() {
return mPhase;
}
@@ -114,6 +125,7 @@ public final class CalculateParams {
/**
* Gets the current {@link LaunchParamsWrapper}.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public LaunchParamsWrapper getCurrentParams() {
return mCurrentParams;
}
@@ -121,6 +133,7 @@ public final class CalculateParams {
/**
* Gets the resulting {@link LaunchParamsWrapper}.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public LaunchParamsWrapper getOutParams() {
return mOutParams;
}
@@ -128,6 +141,7 @@ public final class CalculateParams {
/**
* Returns whether the current system supports the multiple display.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public boolean supportsMultiDisplay() {
return mSupportsMultiDisplay;
}
diff --git a/builtInServices/src/com/android/server/wm/CarActivityInterceptorInterface.java b/builtInServices/src/com/android/server/wm/CarActivityInterceptorInterface.java
new file mode 100644
index 0000000..fed022e
--- /dev/null
+++ b/builtInServices/src/com/android/server/wm/CarActivityInterceptorInterface.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
+
+import com.android.annotation.AddedIn;
+
+/**
+ * Interface implemented by {@link com.android.internal.car.CarActivityInterceptor} and used by
+ * {@link CarActivityInterceptorUpdatable}.
+ *
+ * Because {@code CarActivityInterceptorUpdatable} calls {@code CarActivityInterceptorInterface}
+ * with {@code mLock} acquired, {@code CarActivityInterceptorInterface} shouldn't call
+ * {@code CarActivityInterceptorUpdatable} again during its execution.
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public interface CarActivityInterceptorInterface {
+ /**
+ * Returns the main user (i.e., not a profile) that is assigned to the display, or the
+ * {@link android.app.ActivityManager#getCurrentUser() current foreground user} if no user is
+ * associated with the display.
+ * See {@link com.android.server.pm.UserManagerInternal#getUserAssignedToDisplay(int)} for
+ * the detail.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ @UserIdInt int getUserAssignedToDisplay(int displayId);
+
+ /**
+ * Returns the main display id assigned to the user, or {@code Display.INVALID_DISPLAY} if the
+ * user is not assigned to any main display.
+ * See {@link com.android.server.pm.UserManagerInternal#getMainDisplayAssignedToUser(int)} for
+ * the detail.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int getMainDisplayAssignedToUser(@UserIdInt int userId);
+}
diff --git a/builtInServices/src/com/android/server/wm/CarActivityInterceptorUpdatable.java b/builtInServices/src/com/android/server/wm/CarActivityInterceptorUpdatable.java
new file mode 100644
index 0000000..2e60a55
--- /dev/null
+++ b/builtInServices/src/com/android/server/wm/CarActivityInterceptorUpdatable.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
+
+import com.android.annotation.AddedIn;
+
+/**
+ * Updatable interface of CarActivityInterceptor.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public interface CarActivityInterceptorUpdatable {
+ /**
+ * Intercepts the activity launch.
+ *
+ * @param info the activity info of the activity being launched.
+ * @return the result of the interception in the form of the modified intent & activity options.
+ * {@code null} is returned when no modification is required on intent or activity
+ * options.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ @Nullable
+ ActivityInterceptResultWrapper onInterceptActivityLaunch(
+ ActivityInterceptorInfoWrapper info);
+}
diff --git a/builtInServices/src/com/android/server/wm/CarDisplayAreaPolicyProvider.java b/builtInServices/src/com/android/server/wm/CarDisplayAreaPolicyProvider.java
index ea4a4bf..96a90bc 100644
--- a/builtInServices/src/com/android/server/wm/CarDisplayAreaPolicyProvider.java
+++ b/builtInServices/src/com/android/server/wm/CarDisplayAreaPolicyProvider.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
@@ -83,14 +84,17 @@ public class CarDisplayAreaPolicyProvider implements DisplayAreaPolicy.Provider
TaskDisplayArea backgroundTaskDisplayArea = new TaskDisplayArea(content, wmService,
"BackgroundTaskDisplayArea", BACKGROUND_TASK_CONTAINER,
/* createdByOrganizer= */ false, /* canHostHomeTask= */ false);
+ backgroundTaskDisplayArea.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
TaskDisplayArea controlBarDisplayArea = new TaskDisplayArea(content, wmService,
"ControlBarTaskDisplayArea", CONTROL_BAR_DISPLAY_AREA,
/* createdByOrganizer= */ false, /* canHostHomeTask= */ false);
+ controlBarDisplayArea.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
TaskDisplayArea voicePlateTaskDisplayArea = new TaskDisplayArea(content, wmService,
"VoicePlateTaskDisplayArea", FEATURE_VOICE_PLATE,
/* createdByOrganizer= */ false, /* canHostHomeTask= */ false);
+ // voicePlatTaskDisplayArea needs to be in full screen windowing mode.
List<TaskDisplayArea> backgroundTdaList = new ArrayList<>();
backgroundTdaList.add(voicePlateTaskDisplayArea);
@@ -115,6 +119,8 @@ public class CarDisplayAreaPolicyProvider implements DisplayAreaPolicy.Provider
// Default application launches here
RootDisplayArea defaultAppsRoot = new DisplayAreaGroup(wmService,
"FeatureForegroundApplication", FOREGROUND_DISPLAY_AREA_ROOT);
+ defaultAppsRoot.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+
TaskDisplayArea defaultAppTaskDisplayArea = new TaskDisplayArea(content, wmService,
"DefaultApplicationTaskDisplayArea", DEFAULT_APP_TASK_CONTAINER);
List<TaskDisplayArea> firstTdaList = new ArrayList<>();
diff --git a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifier.java b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifier.java
index 6472ffb..5feb93c 100644
--- a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifier.java
+++ b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifier.java
@@ -28,8 +28,12 @@ import android.content.pm.ActivityInfo;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
+import android.util.Pair;
import android.view.Display;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
import java.util.ArrayList;
import java.util.List;
@@ -85,6 +89,11 @@ public final class CarLaunchParamsModifier implements LaunchParamsController.Lau
new Handler(Looper.getMainLooper()));
}
+ /** Notifies user visibility changed. */
+ public void handleUserVisibilityChanged(int userId, boolean visible) {
+ mUpdatable.handleUserVisibilityChanged(userId, visible);
+ }
+
/** Notifies user switching. */
public void handleUserStarting(@UserIdInt int startingUserId) {
mUpdatable.handleUserStarting(startingUserId);
@@ -158,7 +167,7 @@ public final class CarLaunchParamsModifier implements LaunchParamsController.Lau
mFallBackDisplayAreaList.clear();
WindowProcessController controllerFromLaunchingRecord = mAtm.getProcessController(
- activityRecord.launchedFromPid, activityRecord.launchedFromUid);
+ activityRecord.getLaunchedFromPid(), activityRecord.getLaunchedFromUid());
TaskDisplayArea displayAreaForLaunchingRecord = controllerFromLaunchingRecord == null
? null : controllerFromLaunchingRecord.getTopActivityDisplayArea();
if (displayAreaForLaunchingRecord != null) {
@@ -188,7 +197,7 @@ public final class CarLaunchParamsModifier implements LaunchParamsController.Lau
@Nullable
private TaskDisplayAreaWrapper findTaskDisplayArea(int displayId, int featureId) {
- DisplayContent display = mAtm.mRootWindowContainer.getDisplayContentOrCreate(displayId);
+ DisplayContent display = mAtm.mRootWindowContainer.getDisplayContent(displayId);
if (display == null) {
return null;
}
@@ -197,8 +206,8 @@ public final class CarLaunchParamsModifier implements LaunchParamsController.Lau
return TaskDisplayAreaWrapper.create(tda);
}
- private final CarLaunchParamsModifierInterface mBuiltinInterface
- = new CarLaunchParamsModifierInterface() {
+ private final CarLaunchParamsModifierInterface mBuiltinInterface =
+ new CarLaunchParamsModifierInterface() {
@Nullable
@Override
public TaskDisplayAreaWrapper findTaskDisplayArea(int displayId, int featureId) {
@@ -218,5 +227,25 @@ public final class CarLaunchParamsModifier implements LaunchParamsController.Lau
return CarLaunchParamsModifier.this.getFallbackDisplayAreasForActivity(
activityRecord, request);
}
+
+ @NonNull
+ @Override
+ public Pair<Integer, Integer> getCurrentAndTargetUserIds() {
+ return mAtm.mAmInternal.getCurrentAndTargetUserIds();
+ }
+
+ @Override
+ public int getUserAssignedToDisplay(int displayId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int userId = umi.getUserAssignedToDisplay(displayId);
+ return userId;
+ }
+
+ @Override
+ public int getMainDisplayAssignedToUser(int userId) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ int displayId = umi.getMainDisplayAssignedToUser(userId);
+ return displayId;
+ }
};
}
diff --git a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierInterface.java b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierInterface.java
index 8550c55..ffe3b1f 100644
--- a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierInterface.java
+++ b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierInterface.java
@@ -18,9 +18,14 @@ package com.android.server.wm;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
-import android.graphics.Rect;
-import android.view.Display;
+import android.annotation.UserIdInt;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.Build;
+import android.util.Pair;
+
+import com.android.annotation.AddedIn;
import java.util.List;
@@ -39,17 +44,49 @@ public interface CarLaunchParamsModifierInterface {
* Returns {@link TaskDisplayAreaWrapper} of the given {@code featureId} in the given
* {@code displayId}.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
@Nullable TaskDisplayAreaWrapper findTaskDisplayArea(int displayId, int featureId);
/**
* Returns the default {@link TaskDisplayAreaWrapper} of the given {@code displayId}.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
@Nullable TaskDisplayAreaWrapper getDefaultTaskDisplayAreaOnDisplay(int displayId);
/**
* Returns the list of fallback {@link TaskDisplayAreaWrapper} from the source of the request.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
@NonNull List<TaskDisplayAreaWrapper> getFallbackDisplayAreasForActivity(
@NonNull ActivityRecordWrapper activityRecord, @Nullable RequestWrapper request);
+ /**
+ * @return a pair of the current userId and the target userId.
+ * The target userId is the user to switch during switching the driver,
+ * or {@link android.os.UserHandle.USER_NULL}.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ @NonNull Pair<Integer, Integer> getCurrentAndTargetUserIds();
+
+ /**
+ * Returns the main user (i.e., not a profile) that is assigned to the display, or the
+ * {@link android.app.ActivityManager#getCurrentUser() current foreground user} if no user is
+ * associated with the display.
+ * See {@link com.android.server.pm.UserManagerInternal#getUserAssignedToDisplay(int)} for
+ * the detail.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ @UserIdInt int getUserAssignedToDisplay(int displayId);
+
+ /**
+ * Returns the main display id assigned to the user, or {@code Display.INVALID_DISPLAY} if the
+ * user is not assigned to any main display.
+ * See {@link com.android.server.pm.UserManagerInternal#getMainDisplayAssignedToUser(int)} for
+ * the detail.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ int getMainDisplayAssignedToUser(@UserIdInt int userId);
}
diff --git a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatable.java b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatable.java
index 3abf54e..a381e34 100644
--- a/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatable.java
+++ b/builtInServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatable.java
@@ -16,27 +16,14 @@
package com.android.server.wm;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
import android.annotation.UserIdInt;
-import android.car.app.CarActivityManager;
-import android.content.ComponentName;
+import android.car.builtin.annotation.PlatformVersion;
import android.hardware.display.DisplayManager;
-import android.os.ServiceSpecificException;
-import android.os.UserHandle;
-import android.util.ArrayMap;
-import android.util.Slog;
-import android.util.SparseIntArray;
-import android.view.Display;
-import android.window.DisplayAreaOrganizer;
+import android.os.Build;
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
+import com.android.annotation.AddedIn;
/**
* Updatable interface of CarLaunchParamsModifier.
@@ -46,20 +33,30 @@ import java.util.List;
public interface CarLaunchParamsModifierUpdatable {
/** Returns {@link DisplayManager.DisplayListener} of CarLaunchParamsModifierUpdatable. */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
DisplayManager.DisplayListener getDisplayListener();
/** Notifies user switching. */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ void handleUserVisibilityChanged(@UserIdInt int userId, boolean visible);
+
+ /** Notifies user switching. */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void handleCurrentUserSwitching(@UserIdInt int newUserId);
/** Notifies user starting. */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void handleUserStarting(@UserIdInt int startingUser);
/** Notifies user stopped. */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
void handleUserStopped(@UserIdInt int stoppedUser);
/**
* Calculates {@code outParams} based on the given arguments.
* See {@code LaunchParamsController.LaunchParamsModifier.onCalculate()} for the detail.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
int calculate(CalculateParams params);
}
diff --git a/builtInServices/src/com/android/server/wm/LaunchParamsWrapper.java b/builtInServices/src/com/android/server/wm/LaunchParamsWrapper.java
index 344961f..5f1f696 100644
--- a/builtInServices/src/com/android/server/wm/LaunchParamsWrapper.java
+++ b/builtInServices/src/com/android/server/wm/LaunchParamsWrapper.java
@@ -18,8 +18,11 @@ package com.android.server.wm;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
import android.graphics.Rect;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of {@link com.android.server.wm.LaunchParamsController.LaunchParams}.
* @hide
@@ -27,16 +30,19 @@ import android.graphics.Rect;
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public final class LaunchParamsWrapper {
/** Returned when the modifier does not want to influence the bounds calculation */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static int RESULT_SKIP = LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
/**
* Returned when the modifier has changed the bounds and would like its results to be the
* final bounds applied.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static int RESULT_DONE = LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
/**
* Returned when the modifier has changed the bounds but is okay with other modifiers
* influencing the bounds.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static int RESULT_CONTINUE = LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
private final LaunchParamsController.LaunchParams mLaunchParams;
@@ -46,6 +52,7 @@ public final class LaunchParamsWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static LaunchParamsWrapper create(
@Nullable LaunchParamsController.LaunchParams launchParams) {
if (launchParams == null) return null;
@@ -55,6 +62,7 @@ public final class LaunchParamsWrapper {
/**
* Gets the {@link TaskDisplayAreaWrapper} the {@link Task} would prefer to be on.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayAreaWrapper getPreferredTaskDisplayArea() {
return TaskDisplayAreaWrapper.create(mLaunchParams.mPreferredTaskDisplayArea);
}
@@ -62,6 +70,7 @@ public final class LaunchParamsWrapper {
/**
* Sets the {@link TaskDisplayAreaWrapper} the {@link Task} would prefer to be on.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public void setPreferredTaskDisplayArea(TaskDisplayAreaWrapper tda) {
mLaunchParams.mPreferredTaskDisplayArea = tda.getTaskDisplayArea();
}
@@ -69,6 +78,7 @@ public final class LaunchParamsWrapper {
/**
* Gets the windowing mode to be in.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public int getWindowingMode() {
return mLaunchParams.mWindowingMode;
}
@@ -76,6 +86,7 @@ public final class LaunchParamsWrapper {
/**
* Sets the windowing mode to be in.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public void setWindowingMode(int windowingMode) {
mLaunchParams.mWindowingMode = windowingMode;
}
@@ -83,6 +94,7 @@ public final class LaunchParamsWrapper {
/**
* Gets the bounds within the parent container.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public Rect getBounds() {
return mLaunchParams.mBounds;
}
@@ -90,6 +102,7 @@ public final class LaunchParamsWrapper {
/**
* Sets the bounds within the parent container.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public void setBounds(Rect bounds) {
mLaunchParams.mBounds.set(bounds);
}
diff --git a/builtInServices/src/com/android/server/wm/RequestWrapper.java b/builtInServices/src/com/android/server/wm/RequestWrapper.java
index d6e1795..c5df2d9 100644
--- a/builtInServices/src/com/android/server/wm/RequestWrapper.java
+++ b/builtInServices/src/com/android/server/wm/RequestWrapper.java
@@ -18,6 +18,9 @@ package com.android.server.wm;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
+
+import com.android.annotation.AddedIn;
/**
* Wrapper of {@link com.android.server.wm.ActivityStarter.Request}.
@@ -32,12 +35,14 @@ public final class RequestWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static RequestWrapper create(@Nullable ActivityStarter.Request request) {
if (request == null) return null;
return new RequestWrapper(request);
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public ActivityStarter.Request getRequest() {
return mRequest;
}
diff --git a/builtInServices/src/com/android/server/wm/TaskDisplayAreaWrapper.java b/builtInServices/src/com/android/server/wm/TaskDisplayAreaWrapper.java
index 9faef06..65c7ef3 100644
--- a/builtInServices/src/com/android/server/wm/TaskDisplayAreaWrapper.java
+++ b/builtInServices/src/com/android/server/wm/TaskDisplayAreaWrapper.java
@@ -18,8 +18,11 @@ package com.android.server.wm;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
import android.view.Display;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of {@link TaskDisplayArea}.
* @hide
@@ -33,12 +36,14 @@ public final class TaskDisplayAreaWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static TaskDisplayAreaWrapper create(@Nullable TaskDisplayArea taskDisplayArea) {
if (taskDisplayArea == null) return null;
return new TaskDisplayAreaWrapper(taskDisplayArea);
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayArea getTaskDisplayArea() {
return mTaskDisplayArea;
}
@@ -46,8 +51,9 @@ public final class TaskDisplayAreaWrapper {
/**
* Gets the display this {@link TaskDisplayAreaWrapper} is on.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public Display getDisplay() {
- return mTaskDisplayArea.getDisplayContent().getDisplay();
+ return mTaskDisplayArea.mDisplayContent.getDisplay();
}
@Override
diff --git a/builtInServices/src/com/android/server/wm/TaskWrapper.java b/builtInServices/src/com/android/server/wm/TaskWrapper.java
index 3a2c682..089a6eb 100644
--- a/builtInServices/src/com/android/server/wm/TaskWrapper.java
+++ b/builtInServices/src/com/android/server/wm/TaskWrapper.java
@@ -16,8 +16,16 @@
package com.android.server.wm;
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
+import android.os.IBinder;
+
+import com.android.annotation.AddedIn;
/**
* Wrapper of {@link Task}.
@@ -32,14 +40,23 @@ public final class TaskWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static TaskWrapper create(@Nullable Task task) {
if (task == null) return null;
return new TaskWrapper(task);
}
+ /** Creates an instance of {@link TaskWrapper} based on the task's remote {@code token}. */
+ @AddedIn(PlatformVersion.UPSIDE_DOWN_CAKE_0)
+ @RequiresApi(UPSIDE_DOWN_CAKE)
+ public static TaskWrapper createFromToken(@NonNull IBinder token) {
+ return new TaskWrapper((Task) WindowContainer.fromBinder(token));
+ }
+
/**
* Gets the {@code userId} of this {@link Task} is created for
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public int getUserId() {
return mTask.mUserId;
}
@@ -47,6 +64,7 @@ public final class TaskWrapper {
/**
* Gets the root {@link TaskWrapper} of the this.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskWrapper getRootTask() {
return create(mTask.getRootTask());
}
@@ -54,6 +72,7 @@ public final class TaskWrapper {
/**
* Gets the {@link TaskDisplayAreaWrapper} this {@link Task} is on.
*/
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public TaskDisplayAreaWrapper getTaskDisplayArea() {
return TaskDisplayAreaWrapper.create(mTask.getTaskDisplayArea());
}
diff --git a/builtInServices/src/com/android/server/wm/WindowLayoutWrapper.java b/builtInServices/src/com/android/server/wm/WindowLayoutWrapper.java
index e028a20..5e8a31e 100644
--- a/builtInServices/src/com/android/server/wm/WindowLayoutWrapper.java
+++ b/builtInServices/src/com/android/server/wm/WindowLayoutWrapper.java
@@ -18,8 +18,11 @@ package com.android.server.wm;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.car.builtin.annotation.PlatformVersion;
import android.content.pm.ActivityInfo;
+import com.android.annotation.AddedIn;
+
/**
* Wrapper of {@link android.content.pm.ActivityInfo.WindowLayout}.
* @hide
@@ -33,6 +36,7 @@ public final class WindowLayoutWrapper {
}
/** @hide */
+ @AddedIn(PlatformVersion.TIRAMISU_0)
public static WindowLayoutWrapper create(@Nullable ActivityInfo.WindowLayout layout) {
if (layout == null) return null;
return new WindowLayoutWrapper(layout);
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/AutofillController.java b/builtInServices/src_imms/com/android/server/inputmethod/AutofillController.java
new file mode 100644
index 0000000..83ecd3e
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/AutofillController.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.UserIdInt;
+
+import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
+import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
+
+/**
+ * Interface for Android Auto autofill controllers.
+ */
+public interface AutofillController {
+
+ /**
+ * Fill the autofill suggestion request passed as argument. Starts an autofill Session with the
+ * current IME.
+ */
+ void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
+ InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
+ boolean touchExplorationEnabled);
+
+ /**
+ * Send the autofill suggestions request. The callback passed in
+ * {@link #onCreateInlineSuggestionsRequest} will be invoked with retrieved suggestions.
+ */
+ void performOnCreateInlineSuggestionsRequest();
+
+ /**
+ * Closes the autofill session with IME.
+ */
+ void invalidateAutofillSession();
+}
+
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarAutofillSuggestionsController.java b/builtInServices/src_imms/com/android/server/inputmethod/CarAutofillSuggestionsController.java
new file mode 100644
index 0000000..8a9bbd0
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarAutofillSuggestionsController.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.autofill.AutofillId;
+import android.view.inputmethod.InlineSuggestionsRequest;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
+import com.android.internal.inputmethod.IInlineSuggestionsResponseCallback;
+import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
+
+/**
+ * A controller managing autofill suggestion requests.
+ */
+final class CarAutofillSuggestionsController implements AutofillController {
+ private static final boolean DEBUG = false;
+ private static final String TAG = CarAutofillSuggestionsController.class.getSimpleName();
+
+ @NonNull private final CarInputMethodManagerService mService;
+ @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap;
+ @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
+
+ private static final class CreateInlineSuggestionsRequest {
+ @NonNull final InlineSuggestionsRequestInfo mRequestInfo;
+ @NonNull final IInlineSuggestionsRequestCallback mCallback;
+ @NonNull final String mPackageName;
+
+ CreateInlineSuggestionsRequest(
+ @NonNull InlineSuggestionsRequestInfo requestInfo,
+ @NonNull IInlineSuggestionsRequestCallback callback,
+ @NonNull String packageName) {
+ mRequestInfo = requestInfo;
+ mCallback = callback;
+ mPackageName = packageName;
+ }
+ }
+
+ /**
+ * If a request to create inline autofill suggestions comes in while the IME is unbound
+ * due to {@link CarInputMethodManagerService#mPreventImeStartupUnlessTextEditor},
+ * this is where it is stored, so that it may be fulfilled once the IME rebinds.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private CreateInlineSuggestionsRequest mPendingInlineSuggestionsRequest;
+
+ /**
+ * A callback into the autofill service obtained from the latest call to
+ * {@link #onCreateInlineSuggestionsRequest}, which can be used to invalidate an
+ * autofill session in case the IME process dies.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private IInlineSuggestionsRequestCallback mInlineSuggestionsRequestCallback;
+
+ CarAutofillSuggestionsController(@NonNull CarInputMethodManagerService service) {
+ mService = service;
+ mMethodMap = mService.mMethodMap;
+ mSettings = mService.mSettings;
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
+ InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
+ boolean touchExplorationEnabled) {
+ clearPendingInlineSuggestionsRequest();
+ mInlineSuggestionsRequestCallback = callback;
+ final InputMethodInfo imi = mMethodMap.get(mService.getSelectedMethodIdLocked());
+ try {
+ if (userId == mSettings.getCurrentUserId()
+ && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
+ mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
+ requestInfo, callback, imi.getPackageName());
+ if (mService.getCurMethodLocked() != null) {
+ // In the normal case when the IME is connected, we can make the request here.
+ performOnCreateInlineSuggestionsRequest();
+ } else {
+ // Otherwise, the next time the IME connection is established,
+ // InputMethodBindingController.mMainConnection#onServiceConnected() will call
+ // into #performOnCreateInlineSuggestionsRequestLocked() to make the request.
+ if (DEBUG) {
+ Slog.d(TAG, "IME not connected. Delaying inline suggestions request.");
+ }
+ }
+ } else {
+ callback.onInlineSuggestionsUnsupported();
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "RemoteException calling onCreateInlineSuggestionsRequest(): " + e);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ public void performOnCreateInlineSuggestionsRequest() {
+ if (mPendingInlineSuggestionsRequest == null) {
+ return;
+ }
+ IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+ if (DEBUG) {
+ Slog.d(TAG, "Performing onCreateInlineSuggestionsRequest. mCurMethod = " + curMethod);
+ }
+ if (curMethod != null) {
+ final IInlineSuggestionsRequestCallback callback =
+ new InlineSuggestionsRequestCallbackDecorator(
+ mPendingInlineSuggestionsRequest.mCallback,
+ mPendingInlineSuggestionsRequest.mPackageName,
+ mService.getCurTokenDisplayIdLocked(),
+ mService.getCurTokenLocked(),
+ mService);
+ curMethod.onCreateInlineSuggestionsRequest(
+ mPendingInlineSuggestionsRequest.mRequestInfo, callback);
+ } else {
+ Slog.w(TAG, "No IME connected! Abandoning inline suggestions creation request.");
+ }
+ clearPendingInlineSuggestionsRequest();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void clearPendingInlineSuggestionsRequest() {
+ mPendingInlineSuggestionsRequest = null;
+ }
+
+ private static boolean isInlineSuggestionsEnabled(InputMethodInfo imi,
+ boolean touchExplorationEnabled) {
+ return imi.isInlineSuggestionsEnabled()
+ && (!touchExplorationEnabled
+ || imi.supportsInlineSuggestionsWithTouchExploration());
+ }
+
+ @GuardedBy("ImfLock.class")
+ public void invalidateAutofillSession() {
+ if (mInlineSuggestionsRequestCallback != null) {
+ try {
+ mInlineSuggestionsRequestCallback.onInlineSuggestionsSessionInvalidated();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Cannot invalidate autofill session.", e);
+ }
+ }
+ }
+
+ /**
+ * The decorator which validates the host package name in the
+ * {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name.
+ */
+ private static final class InlineSuggestionsRequestCallbackDecorator
+ extends IInlineSuggestionsRequestCallback.Stub {
+ @NonNull private final IInlineSuggestionsRequestCallback mCallback;
+ @NonNull private final String mImePackageName;
+ private final int mImeDisplayId;
+ @NonNull private final IBinder mImeToken;
+ @NonNull private final CarInputMethodManagerService mImms;
+
+ InlineSuggestionsRequestCallbackDecorator(
+ @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName,
+ int displayId, @NonNull IBinder imeToken,
+ @NonNull CarInputMethodManagerService imms) {
+ mCallback = callback;
+ mImePackageName = imePackageName;
+ mImeDisplayId = displayId;
+ mImeToken = imeToken;
+ mImms = imms;
+ }
+
+ @Override
+ public void onInlineSuggestionsUnsupported() throws RemoteException {
+ mCallback.onInlineSuggestionsUnsupported();
+ }
+
+ @Override
+ public void onInlineSuggestionsRequest(InlineSuggestionsRequest request,
+ IInlineSuggestionsResponseCallback callback)
+ throws RemoteException {
+ if (!mImePackageName.equals(request.getHostPackageName())) {
+ throw new SecurityException(
+ "Host package name in the provide request=[" + request.getHostPackageName()
+ + "] doesn't match the IME package name=[" + mImePackageName
+ + "].");
+ }
+ request.setHostDisplayId(mImeDisplayId);
+ mImms.setCurHostInputToken(mImeToken, request.getHostInputToken());
+ mCallback.onInlineSuggestionsRequest(request, callback);
+ }
+
+ @Override
+ public void onInputMethodStartInput(AutofillId imeFieldId) throws RemoteException {
+ mCallback.onInputMethodStartInput(imeFieldId);
+ }
+
+ @Override
+ public void onInputMethodShowInputRequested(boolean requestResult) throws RemoteException {
+ mCallback.onInputMethodShowInputRequested(requestResult);
+ }
+
+ @Override
+ public void onInputMethodStartInputView() throws RemoteException {
+ mCallback.onInputMethodStartInputView();
+ }
+
+ @Override
+ public void onInputMethodFinishInputView() throws RemoteException {
+ mCallback.onInputMethodFinishInputView();
+ }
+
+ @Override
+ public void onInputMethodFinishInput() throws RemoteException {
+ mCallback.onInputMethodFinishInput();
+ }
+
+ @Override
+ public void onInlineSuggestionsSessionInvalidated() throws RemoteException {
+ mCallback.onInlineSuggestionsSessionInvalidated();
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarDefaultImeVisibilityApplier.java b/builtInServices/src_imms/com/android/server/inputmethod/CarDefaultImeVisibilityApplier.java
new file mode 100644
index 0000000..0b7f4c4
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarDefaultImeVisibilityApplier.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY;
+
+import static com.android.server.EventLogTags.IMF_HIDE_IME;
+import static com.android.server.EventLogTags.IMF_SHOW_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_REMOVE_IME_SNAPSHOT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_IMPLICIT;
+import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME_SNAPSHOT;
+
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.util.EventLog;
+import android.util.Slog;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.server.LocalServices;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.util.Objects;
+
+/**
+ * The default implementation of {@link ImeVisibilityApplier} used in
+ * {@link CarInputMethodManagerService}.
+ */
+final class CarDefaultImeVisibilityApplier implements ImeVisibilityApplier {
+
+ private static final String TAG = "DefaultImeVisibilityApplier";
+
+ private static final boolean DEBUG = CarInputMethodManagerService.DEBUG;
+
+ private CarInputMethodManagerService mService;
+
+ private final WindowManagerInternal mWindowManagerInternal;
+
+
+ CarDefaultImeVisibilityApplier(CarInputMethodManagerService service) {
+ mService = service;
+ mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+ int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+ if (curMethod != null) {
+ if (DEBUG) {
+ Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
+ + ", " + showFlags + ", " + resultReceiver + ") for reason: "
+ + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+ // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
+ if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) {
+ if (DEBUG_IME_VISIBILITY) {
+ EventLog.writeEvent(IMF_SHOW_IME, statsToken.getTag(),
+ Objects.toString(mService.mCurFocusedWindow),
+ InputMethodDebug.softInputDisplayReasonToString(reason),
+ InputMethodDebug.softInputModeToString(
+ mService.mCurFocusedWindowSoftInputMode));
+ }
+ mService.onShowHideSoftInputRequested(true /* show */, showInputToken, reason,
+ statsToken);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void performHideIme(IBinder hideInputToken, @Nullable ImeTracker.Token statsToken,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+ if (curMethod != null) {
+ // The IME will report its visible state again after the following message finally
+ // delivered to the IME process as an IPC. Hence the inconsistency between
+ // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
+ // the final state.
+ if (DEBUG) {
+ Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken
+ + ", " + resultReceiver + ") for reason: "
+ + InputMethodDebug.softInputDisplayReasonToString(reason));
+ }
+ // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not.
+ if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) {
+ if (DEBUG_IME_VISIBILITY) {
+ EventLog.writeEvent(IMF_HIDE_IME, statsToken.getTag(),
+ Objects.toString(mService.mCurFocusedWindow),
+ InputMethodDebug.softInputDisplayReasonToString(reason),
+ InputMethodDebug.softInputModeToString(
+ mService.mCurFocusedWindowSoftInputMode));
+ }
+ mService.onShowHideSoftInputRequested(false /* show */, hideInputToken, reason,
+ statsToken);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Override
+ public void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ @ImeVisibilityStateComputer.VisibilityState int state) {
+ applyImeVisibility(windowToken, statsToken, state, -1 /* ignore reason */);
+ }
+
+ @GuardedBy("ImfLock.class")
+ void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ @ImeVisibilityStateComputer.VisibilityState int state, int reason) {
+ switch (state) {
+ case STATE_SHOW_IME:
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+ // Send to window manager to show IME after IME layout finishes.
+ mWindowManagerInternal.showImePostLayout(windowToken, statsToken);
+ break;
+ case STATE_HIDE_IME:
+ if (mService.hasAttachedClient()) {
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+ // IMMS only knows of focused window, not the actual IME target.
+ // e.g. it isn't aware of any window that has both
+ // NOT_FOCUSABLE, ALT_FOCUSABLE_IM flags set and can the IME target.
+ // Send it to window manager to hide IME from IME target window.
+ // Send it to window manager to hide IME from the actual IME control target
+ // of the target display.
+ mWindowManagerInternal.hideIme(windowToken,
+ mService.getDisplayIdToShowImeLocked(), statsToken);
+ } else {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+ }
+ break;
+ case STATE_HIDE_IME_EXPLICIT:
+ mService.hideCurrentInputLocked(windowToken, statsToken, 0, null, reason);
+ break;
+ case STATE_HIDE_IME_NOT_ALWAYS:
+ mService.hideCurrentInputLocked(windowToken, statsToken,
+ InputMethodManager.HIDE_NOT_ALWAYS, null, reason);
+ break;
+ case STATE_SHOW_IME_IMPLICIT:
+ mService.showCurrentInputLocked(windowToken, statsToken,
+ InputMethodManager.SHOW_IMPLICIT, null, reason);
+ break;
+ case STATE_SHOW_IME_SNAPSHOT:
+ showImeScreenshot(windowToken, mService.getDisplayIdToShowImeLocked(), null);
+ break;
+ case STATE_REMOVE_IME_SNAPSHOT:
+ removeImeScreenshot(mService.getDisplayIdToShowImeLocked());
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid IME visibility state: " + state);
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarImeVisibilityStateComputer.java b/builtInServices/src_imms/com/android/server/inputmethod/CarImeVisibilityStateComputer.java
new file mode 100644
index 0000000..101709e
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarImeVisibilityStateComputer.java
@@ -0,0 +1,738 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+
+import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN;
+import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD;
+import static android.server.inputmethod.InputMethodManagerServiceProto.INPUT_SHOWN;
+import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED;
+import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+
+import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString;
+import static com.android.internal.inputmethod.SoftInputShowHideReason.REMOVE_IME_SCREENSHOT_FROM_IMMS;
+import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_IME_SCREENSHOT_FROM_IMMS;
+import static com.android.server.inputmethod.CarInputMethodManagerService.computeImeDisplayIdForTarget;
+
+import android.accessibilityservice.AccessibilityService;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.res.Configuration;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.PrintWriterPrinter;
+import android.util.Printer;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+import android.view.WindowManager;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.server.LocalServices;
+import com.android.server.wm.ImeTargetChangeListener;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.io.PrintWriter;
+import java.util.WeakHashMap;
+
+/**
+ * A computer used by {@link CarInputMethodManagerService} that computes the IME visibility state
+ * according the given {@link ImeTargetWindowState} from the focused window or the app requested IME
+ * visibility from {@link InputMethodManager}.
+ */
+public final class CarImeVisibilityStateComputer {
+
+ private static final String TAG = "ImeVisibilityStateComputer";
+
+ private static final boolean DEBUG = CarInputMethodManagerService.DEBUG;
+
+ private final CarInputMethodManagerService mService;
+ private final WindowManagerInternal mWindowManagerInternal;
+
+ final CarInputMethodManagerService.ImeDisplayValidator mImeDisplayValidator;
+
+ /**
+ * A map used to track the requested IME target window and its state. The key represents the
+ * token of the window and the value is the corresponding IME window state.
+ */
+ private final WeakHashMap<IBinder, ImeTargetWindowState> mRequestWindowStateMap =
+ new WeakHashMap<>();
+
+ /**
+ * Set if IME was explicitly told to show the input method.
+ *
+ * @see InputMethodManager#SHOW_IMPLICIT that we set the value is {@code false}.
+ * @see InputMethodManager#HIDE_IMPLICIT_ONLY that system will not hide IME when the value is
+ * {@code true}.
+ */
+ boolean mRequestedShowExplicitly;
+
+ /**
+ * Set if we were forced to be shown.
+ *
+ * @see InputMethodManager#SHOW_FORCED
+ * @see InputMethodManager#HIDE_NOT_ALWAYS
+ */
+ boolean mShowForced;
+
+ /**
+ * Set if we last told the input method to show itself.
+ */
+ private boolean mInputShown;
+
+ /**
+ * Set if we called
+ * {@link com.android.server.wm.ImeTargetVisibilityPolicy#showImeScreenshot(IBinder, int)}.
+ */
+ private boolean mRequestedImeScreenshot;
+
+ /** The window token of the current visible IME layering target overlay. */
+ private IBinder mCurVisibleImeLayeringOverlay;
+
+ /** The window token of the current visible IME input target. */
+ private IBinder mCurVisibleImeInputTarget;
+
+ /** Represent the invalid IME visibility state */
+ public static final int STATE_INVALID = -1;
+
+ /** State to handle hiding the IME window requested by the app. */
+ public static final int STATE_HIDE_IME = 0;
+
+ /** State to handle showing the IME window requested by the app. */
+ public static final int STATE_SHOW_IME = 1;
+
+ /** State to handle showing the IME window with making the overlay window above it. */
+ public static final int STATE_SHOW_IME_ABOVE_OVERLAY = 2;
+
+ /** State to handle showing the IME window with making the overlay window behind it. */
+ public static final int STATE_SHOW_IME_BEHIND_OVERLAY = 3;
+
+ /** State to handle showing an IME preview surface during the app was loosing the IME focus */
+ public static final int STATE_SHOW_IME_SNAPSHOT = 4;
+
+ public static final int STATE_HIDE_IME_EXPLICIT = 5;
+
+ public static final int STATE_HIDE_IME_NOT_ALWAYS = 6;
+
+ public static final int STATE_SHOW_IME_IMPLICIT = 7;
+
+ /** State to handle removing an IME preview surface when necessary. */
+ public static final int STATE_REMOVE_IME_SNAPSHOT = 8;
+
+ @IntDef({
+ STATE_INVALID,
+ STATE_HIDE_IME,
+ STATE_SHOW_IME,
+ STATE_SHOW_IME_ABOVE_OVERLAY,
+ STATE_SHOW_IME_BEHIND_OVERLAY,
+ STATE_SHOW_IME_SNAPSHOT,
+ STATE_HIDE_IME_EXPLICIT,
+ STATE_HIDE_IME_NOT_ALWAYS,
+ STATE_SHOW_IME_IMPLICIT,
+ STATE_REMOVE_IME_SNAPSHOT,
+ })
+ @interface VisibilityState {}
+
+ /**
+ * The policy to configure the IME visibility.
+ */
+ private final ImeVisibilityPolicy mPolicy;
+
+ public CarImeVisibilityStateComputer(@NonNull CarInputMethodManagerService service) {
+ this(service,
+ LocalServices.getService(WindowManagerInternal.class),
+ LocalServices.getService(WindowManagerInternal.class)::getDisplayImePolicy,
+ new ImeVisibilityPolicy());
+ }
+
+ @VisibleForTesting
+ public CarImeVisibilityStateComputer(@NonNull CarInputMethodManagerService service,
+ @NonNull Injector injector) {
+ this(service, injector.getWmService(), injector.getImeValidator(),
+ new ImeVisibilityPolicy());
+ }
+
+ interface Injector {
+ default WindowManagerInternal getWmService() {
+ return null;
+ }
+
+ default CarInputMethodManagerService.ImeDisplayValidator getImeValidator() {
+ return null;
+ }
+ }
+
+ private CarImeVisibilityStateComputer(CarInputMethodManagerService service,
+ WindowManagerInternal wmService,
+ CarInputMethodManagerService.ImeDisplayValidator imeDisplayValidator,
+ ImeVisibilityPolicy imePolicy) {
+ mService = service;
+ mWindowManagerInternal = wmService;
+ mImeDisplayValidator = imeDisplayValidator;
+ mPolicy = imePolicy;
+ mWindowManagerInternal.setInputMethodTargetChangeListener(new ImeTargetChangeListener() {
+ @Override
+ public void onImeTargetOverlayVisibilityChanged(IBinder overlayWindowToken,
+ @WindowManager.LayoutParams.WindowType int windowType, boolean visible,
+ boolean removed) {
+ mCurVisibleImeLayeringOverlay =
+ // Ignoring the starting window since it's ok to cover the IME target
+ // window in temporary without affecting the IME visibility.
+ (visible && !removed && windowType != TYPE_APPLICATION_STARTING)
+ ? overlayWindowToken : null;
+ }
+
+ @Override
+ public void onImeInputTargetVisibilityChanged(IBinder imeInputTarget,
+ boolean visibleRequested, boolean removed) {
+ if (mCurVisibleImeInputTarget == imeInputTarget && (!visibleRequested || removed)
+ && mCurVisibleImeLayeringOverlay != null) {
+ mService.onApplyImeVisibilityFromComputer(imeInputTarget,
+ new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_WHEN_INPUT_TARGET_INVISIBLE));
+ }
+ mCurVisibleImeInputTarget = (visibleRequested && !removed) ? imeInputTarget : null;
+ }
+ });
+ }
+
+ /**
+ * Called when {@link CarInputMethodManagerService} is processing the show IME request.
+ * @param statsToken The token for tracking this show request
+ * @param showFlags The additional operation flags to indicate whether this show request mode is
+ * implicit or explicit.
+ * @return {@code true} when the computer has proceed this show request operation.
+ */
+ boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) {
+ if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) {
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
+ return false;
+ }
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
+ if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) {
+ mRequestedShowExplicitly = true;
+ mShowForced = true;
+ } else if ((showFlags & InputMethodManager.SHOW_IMPLICIT) == 0) {
+ mRequestedShowExplicitly = true;
+ }
+ return true;
+ }
+
+ /**
+ * Called when {@link CarInputMethodManagerService} is processing the hide IME request.
+ * @param statsToken The token for tracking this hide request
+ * @param hideFlags The additional operation flags to indicate whether this hide request mode is
+ * implicit or explicit.
+ * @return {@code true} when the computer has proceed this hide request operations.
+ */
+ boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) {
+ if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
+ && (mRequestedShowExplicitly || mShowForced)) {
+ if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT);
+ return false;
+ }
+ if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
+ if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
+ return false;
+ }
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS);
+ return true;
+ }
+
+ int getImeShowFlags() {
+ int flags = 0;
+ if (mShowForced) {
+ flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT;
+ } else if (mRequestedShowExplicitly) {
+ flags |= InputMethod.SHOW_EXPLICIT;
+ } else {
+ flags |= InputMethodManager.SHOW_IMPLICIT;
+ }
+ return flags;
+ }
+
+ void clearImeShowFlags() {
+ mRequestedShowExplicitly = false;
+ mShowForced = false;
+ mInputShown = false;
+ }
+
+ int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) {
+ final int displayToShowIme = computeImeDisplayIdForTarget(displayId, mImeDisplayValidator);
+ state.setImeDisplayId(displayToShowIme);
+ final boolean imeHiddenByPolicy = displayToShowIme == INVALID_DISPLAY;
+ mPolicy.setImeHiddenByDisplayPolicy(imeHiddenByPolicy);
+ return displayToShowIme;
+ }
+
+ /**
+ * Request to show/hide IME from the given window.
+ *
+ * @param windowToken The window which requests to show/hide IME.
+ * @param showIme {@code true} means to show IME, {@code false} otherwise.
+ * Note that in the computer will take this option to compute the
+ * visibility state, it could be {@link #STATE_SHOW_IME} or
+ * {@link #STATE_HIDE_IME}.
+ */
+ void requestImeVisibility(IBinder windowToken, boolean showIme) {
+ ImeTargetWindowState state = getOrCreateWindowState(windowToken);
+ if (!mPolicy.mPendingA11yRequestingHideKeyboard) {
+ state.setRequestedImeVisible(showIme);
+ } else {
+ // As A11y requests no IME is just a temporary, so we don't change the requested IME
+ // visible in case the last visibility state goes wrong after leaving from the a11y
+ // policy.
+ mPolicy.mPendingA11yRequestingHideKeyboard = false;
+ }
+ // create a placeholder token for IMS so that IMS cannot inject windows into client app.
+ state.setRequestImeToken(new Binder());
+ setWindowStateInner(windowToken, state);
+ }
+
+ ImeTargetWindowState getOrCreateWindowState(IBinder windowToken) {
+ ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ if (state == null) {
+ state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNSPECIFIED, 0, false, false, false);
+ }
+ return state;
+ }
+
+ ImeTargetWindowState getWindowStateOrNull(IBinder windowToken) {
+ ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ return state;
+ }
+
+ void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
+ final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ if (state != null && newState.hasEditorFocused()) {
+ // Inherit the last requested IME visible state when the target window is still
+ // focused with an editor.
+ newState.setRequestedImeVisible(state.mRequestedImeVisible);
+ }
+ setWindowStateInner(windowToken, newState);
+ }
+
+ private void setWindowStateInner(IBinder windowToken, @NonNull ImeTargetWindowState newState) {
+ if (DEBUG) {
+ Slog.d(TAG, "setWindowStateInner, windowToken=" + windowToken
+ + ", state=" + newState);
+ }
+ mRequestWindowStateMap.put(windowToken, newState);
+ }
+
+ static class ImeVisibilityResult {
+ private final @VisibilityState int mState;
+ private final @SoftInputShowHideReason int mReason;
+
+ ImeVisibilityResult(@VisibilityState int state, @SoftInputShowHideReason int reason) {
+ mState = state;
+ mReason = reason;
+ }
+
+ @VisibilityState int getState() {
+ return mState;
+ }
+
+ @SoftInputShowHideReason int getReason() {
+ return mReason;
+ }
+ }
+
+ ImeVisibilityResult computeState(ImeTargetWindowState state, boolean allowVisible) {
+ // TODO: Output the request IME visibility state according to the requested window state
+ final int softInputVisibility = state.mSoftInputModeState & SOFT_INPUT_MASK_STATE;
+ // Should we auto-show the IME even if the caller has not
+ // specified what should be done with it?
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ final boolean doAutoShow =
+ (state.mSoftInputModeState & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)
+ == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ || mService.mRes.getConfiguration().isLayoutSizeAtLeast(
+ Configuration.SCREENLAYOUT_SIZE_LARGE);
+ final boolean isForwardNavigation = (state.mSoftInputModeState
+ & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0;
+
+ // We shows the IME when the system allows the IME focused target window to restore the
+ // IME visibility (e.g. switching to the app task when last time the IME is visible).
+ // Note that we don't restore IME visibility for some cases (e.g. when the soft input
+ // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation).
+ // Because the app might leverage these flags to hide soft-keyboard with showing their own
+ // UI for input.
+ if (state.hasEditorFocused() && shouldRestoreImeVisibility(state)) {
+ if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
+ // Inherit the last requested IME visible state when the target window is still
+ // focused with an editor.
+ state.setRequestedImeVisible(true);
+ setWindowStateInner(getWindowTokenFrom(state), state);
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY);
+ }
+
+ switch (softInputVisibility) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
+ if (state.hasImeFocusChanged() && (!state.hasEditorFocused() || !doAutoShow)) {
+ if (WindowManager.LayoutParams.mayUseInputMethod(state.getWindowFlags())) {
+ // There is no focus view, and this window will
+ // be behind any soft input window, so hide the
+ // soft input window if it is shown.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS,
+ SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);
+ }
+ } else if (state.hasEditorFocused() && doAutoShow && isForwardNavigation) {
+ // There is a focus view, and we are navigating forward
+ // into the window, so show the input window for the user.
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+ // Do nothing but preserving the last IME requested visibility state.
+ final ImeTargetWindowState lastState =
+ getWindowStateOrNull(mService.mLastImeTargetWindow);
+ if (lastState != null) {
+ state.setRequestedImeVisible(lastState.mRequestedImeVisible);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ if (isForwardNavigation) {
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ if (state.hasImeFocusChanged()) {
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+ if (isForwardNavigation) {
+ if (allowVisible) {
+ if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
+ }
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+ if (DEBUG) Slog.v(TAG, "Window asks to always show input");
+ if (allowVisible) {
+ if (state.hasImeFocusChanged()) {
+ return new ImeVisibilityResult(STATE_SHOW_IME_IMPLICIT,
+ SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
+ }
+ } else {
+ Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
+ + " there is no focused view that also returns true from"
+ + " View#onCheckIsTextEditor()");
+ }
+ break;
+ }
+
+ if (!state.hasImeFocusChanged()) {
+ // On previous platforms, when Dialogs re-gained focus, the Activity behind
+ // would briefly gain focus first, and dismiss the IME.
+ // On R that behavior has been fixed, but unfortunately apps have come
+ // to rely on this behavior to hide the IME when the editor no longer has focus
+ // To maintain compatibility, we are now hiding the IME when we don't have
+ // an editor upon refocusing a window.
+ if (state.isStartInputByGainFocus()) {
+ if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
+ }
+ }
+ if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus()
+ && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) {
+ // Hide the soft-keyboard when the system do nothing for softInputModeState
+ // of the window being gained focus without an editor. This behavior benefits
+ // to resolve some unexpected IME visible cases while that window with following
+ // configurations being switched from an IME shown window:
+ // 1) SOFT_INPUT_STATE_UNCHANGED state without an editor
+ // 2) SOFT_INPUT_STATE_VISIBLE state without an editor
+ // 3) SOFT_INPUT_STATE_ALWAYS_VISIBLE state without an editor
+ if (DEBUG) Slog.v(TAG, "Window without editor will hide input");
+ return new ImeVisibilityResult(STATE_HIDE_IME_EXPLICIT,
+ SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
+ }
+ return null;
+ }
+
+ @VisibleForTesting
+ ImeVisibilityResult onInteractiveChanged(IBinder windowToken, boolean interactive) {
+ final ImeTargetWindowState state = getWindowStateOrNull(windowToken);
+ if (state != null && state.isRequestedImeVisible() && mInputShown && !interactive) {
+ mRequestedImeScreenshot = true;
+ return new ImeVisibilityResult(STATE_SHOW_IME_SNAPSHOT, SHOW_IME_SCREENSHOT_FROM_IMMS);
+ }
+ if (interactive && mRequestedImeScreenshot) {
+ mRequestedImeScreenshot = false;
+ return new ImeVisibilityResult(STATE_REMOVE_IME_SNAPSHOT,
+ REMOVE_IME_SCREENSHOT_FROM_IMMS);
+ }
+ return null;
+ }
+
+ IBinder getWindowTokenFrom(IBinder requestImeToken) {
+ for (IBinder windowToken : mRequestWindowStateMap.keySet()) {
+ final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ if (state.getRequestImeToken() == requestImeToken) {
+ return windowToken;
+ }
+ }
+ // Fallback to the focused window for some edge cases (e.g. relaunching the activity)
+ return mService.mCurFocusedWindow;
+ }
+
+ IBinder getWindowTokenFrom(ImeTargetWindowState windowState) {
+ for (IBinder windowToken : mRequestWindowStateMap.keySet()) {
+ final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken);
+ if (state == windowState) {
+ return windowToken;
+ }
+ }
+ return null;
+ }
+
+ boolean shouldRestoreImeVisibility(@NonNull ImeTargetWindowState state) {
+ final int softInputMode = state.getSoftInputModeState();
+ switch (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ return false;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
+ return false;
+ }
+ }
+ return mWindowManagerInternal.shouldRestoreImeVisibility(getWindowTokenFrom(state));
+ }
+
+ boolean isInputShown() {
+ return mInputShown;
+ }
+
+ void setInputShown(boolean inputShown) {
+ mInputShown = inputShown;
+ }
+
+ void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ proto.write(SHOW_EXPLICITLY_REQUESTED, mRequestedShowExplicitly);
+ proto.write(SHOW_FORCED, mShowForced);
+ proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD,
+ mPolicy.isA11yRequestNoSoftKeyboard());
+ proto.write(INPUT_SHOWN, mInputShown);
+ }
+
+ void dump(PrintWriter pw) {
+ final Printer p = new PrintWriterPrinter(pw);
+ p.println(" mRequestedShowExplicitly=" + mRequestedShowExplicitly
+ + " mShowForced=" + mShowForced);
+ p.println(" mImeHiddenByDisplayPolicy=" + mPolicy.isImeHiddenByDisplayPolicy());
+ p.println(" mInputShown=" + mInputShown);
+ }
+
+ /**
+ * A settings class to manage all IME related visibility policies or settings.
+ *
+ * This is used for the visibility computer to manage and tell
+ * {@link CarInputMethodManagerService} if the requested IME visibility is valid from
+ * application call or the focus window.
+ */
+ static class ImeVisibilityPolicy {
+ /**
+ * {@code true} if the Ime policy has been set to
+ * {@link WindowManager#DISPLAY_IME_POLICY_HIDE}.
+ *
+ * This prevents the IME from showing when it otherwise may have shown.
+ */
+ private boolean mImeHiddenByDisplayPolicy;
+
+ /**
+ * Set when the accessibility service requests to hide IME by
+ * {@link AccessibilityService.SoftKeyboardController#setShowMode}
+ */
+ private boolean mA11yRequestingNoSoftKeyboard;
+
+ /**
+ * Used when A11y request to hide IME temporary when receiving
+ * {@link AccessibilityService#SHOW_MODE_HIDDEN} from
+ * {@link android.provider.Settings.Secure#ACCESSIBILITY_SOFT_KEYBOARD_MODE} without
+ * changing the requested IME visible state.
+ */
+ private boolean mPendingA11yRequestingHideKeyboard;
+
+ void setImeHiddenByDisplayPolicy(boolean hideIme) {
+ mImeHiddenByDisplayPolicy = hideIme;
+ }
+
+ boolean isImeHiddenByDisplayPolicy() {
+ return mImeHiddenByDisplayPolicy;
+ }
+
+ void setA11yRequestNoSoftKeyboard(int keyboardShowMode) {
+ mA11yRequestingNoSoftKeyboard =
+ (keyboardShowMode & AccessibilityService.SHOW_MODE_MASK) == SHOW_MODE_HIDDEN;
+ if (mA11yRequestingNoSoftKeyboard) {
+ mPendingA11yRequestingHideKeyboard = true;
+ }
+ }
+
+ boolean isA11yRequestNoSoftKeyboard() {
+ return mA11yRequestingNoSoftKeyboard;
+ }
+ }
+
+ ImeVisibilityPolicy getImePolicy() {
+ return mPolicy;
+ }
+
+ /**
+ * A class that represents the current state of the IME target window.
+ */
+ static class ImeTargetWindowState {
+ ImeTargetWindowState(@SoftInputModeFlags int softInputModeState, int windowFlags,
+ boolean imeFocusChanged, boolean hasFocusedEditor,
+ boolean isStartInputByGainFocus) {
+ mSoftInputModeState = softInputModeState;
+ mWindowFlags = windowFlags;
+ mImeFocusChanged = imeFocusChanged;
+ mHasFocusedEditor = hasFocusedEditor;
+ mIsStartInputByGainFocus = isStartInputByGainFocus;
+ }
+
+ /**
+ * Visibility state for this window. By default no state has been specified.
+ */
+ private final @SoftInputModeFlags int mSoftInputModeState;
+
+ private final int mWindowFlags;
+
+ /**
+ * {@code true} means the IME focus changed from the previous window, {@code false}
+ * otherwise.
+ */
+ private final boolean mImeFocusChanged;
+
+ /**
+ * {@code true} when the window has focused an editor, {@code false} otherwise.
+ */
+ private final boolean mHasFocusedEditor;
+
+ private final boolean mIsStartInputByGainFocus;
+
+ /**
+ * Set if the client has asked for the input method to be shown.
+ */
+ private boolean mRequestedImeVisible;
+
+ /**
+ * A identifier for knowing the requester of {@link InputMethodManager#showSoftInput} or
+ * {@link InputMethodManager#hideSoftInputFromWindow}.
+ */
+ private IBinder mRequestImeToken;
+
+ /**
+ * The IME target display id for which the latest startInput was called.
+ */
+ private int mImeDisplayId = DEFAULT_DISPLAY;
+
+ boolean hasImeFocusChanged() {
+ return mImeFocusChanged;
+ }
+
+ boolean hasEditorFocused() {
+ return mHasFocusedEditor;
+ }
+
+ boolean isStartInputByGainFocus() {
+ return mIsStartInputByGainFocus;
+ }
+
+ int getSoftInputModeState() {
+ return mSoftInputModeState;
+ }
+
+ int getWindowFlags() {
+ return mWindowFlags;
+ }
+
+ private void setImeDisplayId(int imeDisplayId) {
+ mImeDisplayId = imeDisplayId;
+ }
+
+ int getImeDisplayId() {
+ return mImeDisplayId;
+ }
+
+ private void setRequestedImeVisible(boolean requestedImeVisible) {
+ mRequestedImeVisible = requestedImeVisible;
+ }
+
+ boolean isRequestedImeVisible() {
+ return mRequestedImeVisible;
+ }
+
+ void setRequestImeToken(IBinder token) {
+ mRequestImeToken = token;
+ }
+
+ IBinder getRequestImeToken() {
+ return mRequestImeToken;
+ }
+
+ @Override
+ public String toString() {
+ return "ImeTargetWindowState{ imeToken " + mRequestImeToken
+ + " imeFocusChanged " + mImeFocusChanged
+ + " hasEditorFocused " + mHasFocusedEditor
+ + " requestedImeVisible " + mRequestedImeVisible
+ + " imeDisplayId " + mImeDisplayId
+ + " softInputModeState " + softInputModeToString(mSoftInputModeState)
+ + " isStartInputByGainFocus " + mIsStartInputByGainFocus
+ + "}";
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodBindingController.java b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodBindingController.java
new file mode 100644
index 0000000..546fc72
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodBindingController.java
@@ -0,0 +1,540 @@
+/*
+ * 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.android.server.inputmethod;
+
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManagerInternal;
+import android.inputmethodservice.InputMethodService;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.EventLog;
+import android.util.Slog;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.IInputMethod;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.UnbindReason;
+import com.android.server.EventLogTags;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A controller managing the state of the input method binding.
+ */
+final class CarInputMethodBindingController {
+ static final boolean DEBUG = false;
+ private static final String TAG = CarInputMethodBindingController.class.getSimpleName();
+
+ /** Time in milliseconds that the IME service has to bind before it is reconnected. */
+ static final long TIME_TO_RECONNECT = 3 * 1000;
+
+ @NonNull private final CarInputMethodManagerService mService;
+ @NonNull private final Context mContext;
+ @NonNull private final ArrayMap<String, InputMethodInfo> mMethodMap;
+ @NonNull private final InputMethodUtils.InputMethodSettings mSettings;
+ @NonNull private final PackageManagerInternal mPackageManagerInternal;
+ @NonNull private final WindowManagerInternal mWindowManagerInternal;
+
+ @GuardedBy("ImfLock.class") private long mLastBindTime;
+ @GuardedBy("ImfLock.class") private boolean mHasConnection;
+ @GuardedBy("ImfLock.class") @Nullable private String mCurId;
+ @GuardedBy("ImfLock.class") @Nullable private String mSelectedMethodId;
+ @GuardedBy("ImfLock.class") @Nullable private Intent mCurIntent;
+ @GuardedBy("ImfLock.class") @Nullable private IInputMethodInvoker mCurMethod;
+ @GuardedBy("ImfLock.class") private int mCurMethodUid = Process.INVALID_UID;
+ @GuardedBy("ImfLock.class") @Nullable private IBinder mCurToken;
+ @GuardedBy("ImfLock.class") private int mCurSeq;
+ @GuardedBy("ImfLock.class") private boolean mVisibleBound;
+ @GuardedBy("ImfLock.class") private boolean mSupportsStylusHw;
+
+ @Nullable private CountDownLatch mLatchForTesting;
+
+ /**
+ * Binding flags for establishing connection to the {@link InputMethodService}.
+ */
+ @VisibleForTesting
+ static final int IME_CONNECTION_BIND_FLAGS =
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_VISIBLE
+ | Context.BIND_NOT_FOREGROUND
+ | Context.BIND_IMPORTANT_BACKGROUND
+ | Context.BIND_SCHEDULE_LIKE_TOP_APP;
+
+ private final int mImeConnectionBindFlags;
+
+ /**
+ * Binding flags used only while the {@link InputMethodService} is showing window.
+ */
+ @VisibleForTesting
+ static final int IME_VISIBLE_BIND_FLAGS =
+ Context.BIND_AUTO_CREATE
+ | Context.BIND_TREAT_LIKE_ACTIVITY
+ | Context.BIND_FOREGROUND_SERVICE
+ | Context.BIND_INCLUDE_CAPABILITIES
+ | Context.BIND_SHOWING_UI;
+
+ CarInputMethodBindingController(@NonNull CarInputMethodManagerService service) {
+ this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
+ }
+
+ CarInputMethodBindingController(@NonNull CarInputMethodManagerService service,
+ int imeConnectionBindFlags, CountDownLatch latchForTesting) {
+ mService = service;
+ mContext = mService.mContext;
+ mMethodMap = mService.mMethodMap;
+ mSettings = mService.mSettings;
+ mPackageManagerInternal = mService.mPackageManagerInternal;
+ mWindowManagerInternal = mService.mWindowManagerInternal;
+ mImeConnectionBindFlags = imeConnectionBindFlags;
+ mLatchForTesting = latchForTesting;
+ }
+
+ /**
+ * Time that we last initiated a bind to the input method, to determine
+ * if we should try to disconnect and reconnect to it.
+ */
+ @GuardedBy("ImfLock.class")
+ long getLastBindTime() {
+ return mLastBindTime;
+ }
+
+ /**
+ * Set to true if our ServiceConnection is currently actively bound to
+ * a service (whether or not we have gotten its IBinder back yet).
+ */
+ @GuardedBy("ImfLock.class")
+ boolean hasConnection() {
+ return mHasConnection;
+ }
+
+ /**
+ * Id obtained with {@link InputMethodInfo#getId()} for the input method that we are currently
+ * connected to or in the process of connecting to.
+ *
+ * <p>This can be {@code null} when no input method is connected.</p>
+ *
+ * @see #getSelectedMethodId()
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ String getCurId() {
+ return mCurId;
+ }
+
+ /**
+ * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method.
+ * This is to be synchronized with the secure settings keyed with
+ * {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD}.
+ *
+ * <p>This can be transiently {@code null} when the system is re-initializing input method
+ * settings, e.g., the system locale is just changed.</p>
+ *
+ * <p>Note that {@link #getCurId()} is used to track which IME is being connected to
+ * {@link com.android.server.inputmethod.CarInputMethodManagerService}.</p>
+ *
+ * @see #getCurId()
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ String getSelectedMethodId() {
+ return mSelectedMethodId;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void setSelectedMethodId(@Nullable String selectedMethodId) {
+ mSelectedMethodId = selectedMethodId;
+ }
+
+ /**
+ * The token we have made for the currently active input method, to
+ * identify it in the future.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ IBinder getCurToken() {
+ return mCurToken;
+ }
+
+ /**
+ * The Intent used to connect to the current input method.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ Intent getCurIntent() {
+ return mCurIntent;
+ }
+
+ /**
+ * The current binding sequence number, incremented every time there is
+ * a new bind performed.
+ */
+ @GuardedBy("ImfLock.class")
+ int getSequenceNumber() {
+ return mCurSeq;
+ }
+
+ /**
+ * Increase the current binding sequence number by one.
+ * Reset to 1 on overflow.
+ */
+ @GuardedBy("ImfLock.class")
+ void advanceSequenceNumber() {
+ mCurSeq += 1;
+ if (mCurSeq <= 0) {
+ mCurSeq = 1;
+ }
+ }
+
+ /**
+ * If non-null, this is the input method service we are currently connected
+ * to.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ IInputMethodInvoker getCurMethod() {
+ return mCurMethod;
+ }
+
+ /**
+ * If not {@link Process#INVALID_UID}, then the UID of {@link #getCurIntent()}.
+ */
+ @GuardedBy("ImfLock.class")
+ int getCurMethodUid() {
+ return mCurMethodUid;
+ }
+
+ /**
+ * Indicates whether {@link #mVisibleConnection} is currently in use.
+ */
+ @GuardedBy("ImfLock.class")
+ boolean isVisibleBound() {
+ return mVisibleBound;
+ }
+
+ /**
+ * Returns {@code true} if current IME supports Stylus Handwriting.
+ */
+ boolean supportsStylusHandwriting() {
+ return mSupportsStylusHw;
+ }
+
+ /**
+ * Used to bring IME service up to visible adjustment while it is being shown.
+ */
+ @GuardedBy("ImfLock.class")
+ private final ServiceConnection mVisibleConnection = new ServiceConnection() {
+ @Override public void onBindingDied(ComponentName name) {
+ synchronized (ImfLock.class) {
+ mService.invalidateAutofillSessionLocked();
+ if (isVisibleBound()) {
+ unbindVisibleConnection();
+ }
+ }
+ }
+
+ @Override public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+
+ @Override public void onServiceDisconnected(ComponentName name) {
+ synchronized (ImfLock.class) {
+ mService.invalidateAutofillSessionLocked();
+ }
+ }
+ };
+
+ /**
+ * Used to bind the IME while it is not currently being shown.
+ */
+ @GuardedBy("ImfLock.class")
+ private final ServiceConnection mMainConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onServiceConnected");
+ synchronized (ImfLock.class) {
+ if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
+ mCurMethod = IInputMethodInvoker.create(IInputMethod.Stub.asInterface(service));
+ updateCurrentMethodUid();
+ if (mCurToken == null) {
+ Slog.w(TAG, "Service connected without a token!");
+ unbindCurrentMethod();
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ return;
+ }
+ if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
+ final InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
+ mSupportsStylusHw = info.supportsStylusHandwriting();
+ mService.initializeImeLocked(mCurMethod, mCurToken);
+ mService.scheduleNotifyImeUidToAudioService(mCurMethodUid);
+ mService.reRequestCurrentClientSessionLocked();
+ mService.performOnCreateInlineSuggestionsRequestLocked();
+ }
+
+ // reset Handwriting event receiver.
+ // always call this as it handles changes in mSupportsStylusHw. It is a noop
+ // if unchanged.
+ mService.scheduleResetStylusHandwriting();
+ }
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+
+ if (mLatchForTesting != null) {
+ mLatchForTesting.countDown(); // Notify the finish to tests
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void updateCurrentMethodUid() {
+ final String curMethodPackage = mCurIntent.getComponent().getPackageName();
+ final int curMethodUid = mPackageManagerInternal.getPackageUid(
+ curMethodPackage, 0 /* flags */, mSettings.getCurrentUserId());
+ if (curMethodUid < 0) {
+ Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
+ mCurMethodUid = Process.INVALID_UID;
+ } else {
+ mCurMethodUid = curMethodUid;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(@NonNull ComponentName name) {
+ // Note that mContext.unbindService(this) does not trigger this. Hence if we are
+ // here the
+ // disconnection is not intended by IMMS (e.g. triggered because the current IMS
+ // crashed),
+ // which is irregular but can eventually happen for everyone just by continuing
+ // using the
+ // device. Thus it is important to make sure that all the internal states are
+ // properly
+ // refreshed when this method is called back. Running
+ // adb install -r <APK that implements the current IME>
+ // would be a good way to trigger such a situation.
+ synchronized (ImfLock.class) {
+ if (DEBUG) {
+ Slog.v(TAG, "Service disconnected: " + name + " mCurIntent=" + mCurIntent);
+ }
+ if (mCurMethod != null && mCurIntent != null
+ && name.equals(mCurIntent.getComponent())) {
+ // We consider this to be a new bind attempt, since the system
+ // should now try to restart the service for us.
+ mLastBindTime = SystemClock.uptimeMillis();
+ clearCurMethodAndSessions();
+ mService.clearInputShownLocked();
+ mService.unbindCurrentClientLocked(UnbindReason.DISCONNECT_IME);
+ }
+ }
+ }
+ };
+
+ @GuardedBy("ImfLock.class")
+ void unbindCurrentMethod() {
+ if (isVisibleBound()) {
+ unbindVisibleConnection();
+ }
+
+ if (hasConnection()) {
+ unbindMainConnection();
+ }
+
+ if (getCurToken() != null) {
+ removeCurrentToken();
+ mService.resetSystemUiLocked();
+ }
+
+ mCurId = null;
+ clearCurMethodAndSessions();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void clearCurMethodAndSessions() {
+ mService.clearClientSessionsLocked();
+ mCurMethod = null;
+ mCurMethodUid = Process.INVALID_UID;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void removeCurrentToken() {
+ int curTokenDisplayId = mService.getCurTokenDisplayIdLocked();
+
+ if (DEBUG) {
+ Slog.v(TAG,
+ "Removing window token: " + mCurToken + " for display: " + curTokenDisplayId);
+ }
+ mWindowManagerInternal.removeWindowToken(mCurToken, false /* removeWindows */,
+ false /* animateExit */, curTokenDisplayId);
+ mCurToken = null;
+ }
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ InputBindResult bindCurrentMethod() {
+ if (mSelectedMethodId == null) {
+ Slog.e(TAG, "mSelectedMethodId is null!");
+ return InputBindResult.NO_IME;
+ }
+
+ InputMethodInfo info = mMethodMap.get(mSelectedMethodId);
+ if (info == null) {
+ throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
+ }
+
+ mCurIntent = createImeBindingIntent(info.getComponent());
+
+ if (bindCurrentInputMethodServiceMainConnection()) {
+ mCurId = info.getId();
+ mLastBindTime = SystemClock.uptimeMillis();
+
+ addFreshWindowToken();
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+ null, null, null, mCurId, mCurSeq, null, false);
+ }
+
+ Slog.w(CarInputMethodManagerService.TAG,
+ "Failure connecting to input method service: " + mCurIntent);
+ mCurIntent = null;
+ return InputBindResult.IME_NOT_CONNECTED;
+ }
+
+ @NonNull
+ private Intent createImeBindingIntent(ComponentName component) {
+ Intent intent = new Intent(InputMethod.SERVICE_INTERFACE);
+ intent.setComponent(component);
+ intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+ com.android.internal.R.string.input_method_binding_label);
+ intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
+ mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
+ PendingIntent.FLAG_IMMUTABLE));
+ return intent;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void addFreshWindowToken() {
+ int displayIdToShowIme = mService.getDisplayIdToShowImeLocked();
+ mCurToken = new Binder();
+
+ mService.setCurTokenDisplayIdLocked(displayIdToShowIme);
+
+ if (DEBUG) {
+ Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
+ + displayIdToShowIme);
+ }
+ mWindowManagerInternal.addWindowToken(mCurToken,
+ WindowManager.LayoutParams.TYPE_INPUT_METHOD,
+ displayIdToShowIme, null /* options */);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void unbindMainConnection() {
+ mContext.unbindService(mMainConnection);
+ mHasConnection = false;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void unbindVisibleConnection() {
+ mContext.unbindService(mVisibleConnection);
+ mVisibleBound = false;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean bindCurrentInputMethodService(ServiceConnection conn, int flags) {
+ if (mCurIntent == null || conn == null) {
+ Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
+ return false;
+ }
+ return mContext.bindServiceAsUser(mCurIntent, conn, flags,
+ new UserHandle(mSettings.getCurrentUserId()));
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean bindCurrentInputMethodServiceMainConnection() {
+ mHasConnection = bindCurrentInputMethodService(mMainConnection, mImeConnectionBindFlags);
+ return mHasConnection;
+ }
+
+ /**
+ * Bind the IME so that it can be shown.
+ *
+ * <p>
+ * Performs a rebind if no binding is achieved in {@link #TIME_TO_RECONNECT} milliseconds.
+ */
+ @GuardedBy("ImfLock.class")
+ void setCurrentMethodVisible() {
+ if (mCurMethod != null) {
+ if (DEBUG) Slog.d(TAG, "setCurrentMethodVisible: mCurToken=" + mCurToken);
+ if (hasConnection() && !isVisibleBound()) {
+ mVisibleBound = bindCurrentInputMethodService(mVisibleConnection,
+ IME_VISIBLE_BIND_FLAGS);
+ }
+ return;
+ }
+
+ // No IME is currently connected. Reestablish the main connection.
+ if (!hasConnection()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Cannot show input: no IME bound. Rebinding.");
+ }
+ bindCurrentMethod();
+ return;
+ }
+
+ long bindingDuration = SystemClock.uptimeMillis() - mLastBindTime;
+ if (bindingDuration >= TIME_TO_RECONNECT) {
+ // The client has asked to have the input method shown, but
+ // we have been sitting here too long with a connection to the
+ // service and no interface received, so let's disconnect/connect
+ // to try to prod things along.
+ EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, getSelectedMethodId(),
+ bindingDuration, 1);
+ Slog.w(TAG, "Force disconnect/connect to the IME in setCurrentMethodVisible()");
+ unbindMainConnection();
+ bindCurrentInputMethodServiceMainConnection();
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Can't show input: connection = " + mHasConnection + ", time = "
+ + (TIME_TO_RECONNECT - bindingDuration));
+ }
+ }
+ }
+
+ /**
+ * Remove the binding needed for the IME to be shown.
+ */
+ @GuardedBy("ImfLock.class")
+ void setCurrentMethodNotVisible() {
+ if (isVisibleBound()) {
+ unbindVisibleConnection();
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodManagerService.java b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodManagerService.java
new file mode 100644
index 0000000..967fa0d
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodManagerService.java
@@ -0,0 +1,6854 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
+import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE;
+import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED;
+import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION;
+import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_CLIENT;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_NAME;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ID;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_METHOD_ID;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_SEQ;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_TOKEN;
+import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_TOKEN_DISPLAY_ID;
+import static android.server.inputmethod.InputMethodManagerServiceProto.HAVE_CONNECTION;
+import static android.server.inputmethod.InputMethodManagerServiceProto.IME_WINDOW_VISIBILITY;
+import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLSCREEN_MODE;
+import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE;
+import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME;
+import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID;
+import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD;
+import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
+import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
+
+import static com.android.server.inputmethod.CarImeVisibilityStateComputer.ImeTargetWindowState;
+import static com.android.server.inputmethod.CarImeVisibilityStateComputer.ImeVisibilityResult;
+import static com.android.server.inputmethod.CarInputMethodBindingController.TIME_TO_RECONNECT;
+import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.annotation.AnyThread;
+import android.annotation.BinderThread;
+import android.annotation.DrawableRes;
+import android.annotation.DurationMillisLong;
+import android.annotation.EnforcePermission;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Matrix;
+import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.InputManager;
+import android.inputmethodservice.InputMethodService;
+import android.media.AudioManagerInternal;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.IndentingPrintWriter;
+import android.util.IntArray;
+import android.util.Pair;
+import android.util.PrintWriterPrinter;
+import android.util.Printer;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.proto.ProtoOutputStream;
+import android.view.DisplayInfo;
+import android.view.InputChannel;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+import android.view.WindowManager.DisplayImePolicy;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceFileProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodManagerServiceTraceFileProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodManagerServiceTraceProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodServiceTraceFileProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodServiceTraceProto;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.inputmethod.DirectBootAwareness;
+import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
+import com.android.internal.inputmethod.IImeTracker;
+import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
+import com.android.internal.inputmethod.IInputContentUriToken;
+import com.android.internal.inputmethod.IInputMethod;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IInputMethodPrivilegedOperations;
+import com.android.internal.inputmethod.IInputMethodSession;
+import com.android.internal.inputmethod.IInputMethodSessionCallback;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.ImeTracing;
+import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.InputMethodDebug;
+import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import com.android.internal.inputmethod.InputMethodSubtypeHandle;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.inputmethod.StartInputFlags;
+import com.android.internal.inputmethod.StartInputReason;
+import com.android.internal.inputmethod.UnbindReason;
+import com.android.internal.os.TransferPipe;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ConcurrentUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.view.IInputMethodManager;
+import com.android.server.AccessibilityManagerInternal;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
+import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
+import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.utils.PriorityDump;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.security.InvalidParameterException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.OptionalInt;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class provides a system service that manages input methods.
+ *
+ * @hide // CarInputMethodManagerService
+ */
+public final class CarInputMethodManagerService extends IInputMethodManager.Stub
+ implements Handler.Callback {
+ // Virtual device id for test.
+ private static final Integer VIRTUAL_STYLUS_ID_FOR_TEST = 999999;
+ static final boolean DEBUG = false;
+ static final String TAG = "CarInputMethodManagerService";
+ public static final String PROTO_ARG = "--proto";
+
+ @Retention(SOURCE)
+ @IntDef({ShellCommandResult.SUCCESS, ShellCommandResult.FAILURE})
+ private @interface ShellCommandResult {
+ int SUCCESS = 0;
+ int FAILURE = -1;
+ }
+
+ private static final int MSG_SHOW_IM_SUBTYPE_PICKER = 1;
+
+ private static final int MSG_HIDE_CURRENT_INPUT_METHOD = 1035;
+ private static final int MSG_REMOVE_IME_SURFACE = 1060;
+ private static final int MSG_REMOVE_IME_SURFACE_FROM_WINDOW = 1061;
+ private static final int MSG_UPDATE_IME_WINDOW_STATUS = 1070;
+
+ private static final int MSG_RESET_HANDWRITING = 1090;
+ private static final int MSG_START_HANDWRITING = 1100;
+ private static final int MSG_FINISH_HANDWRITING = 1110;
+ private static final int MSG_REMOVE_HANDWRITING_WINDOW = 1120;
+
+ private static final int MSG_PREPARE_HANDWRITING_DELEGATION = 1130;
+
+ private static final int MSG_SET_INTERACTIVE = 3030;
+
+ private static final int MSG_HARD_KEYBOARD_SWITCH_CHANGED = 4000;
+
+ private static final int MSG_SYSTEM_UNLOCK_USER = 5000;
+ private static final int MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED = 5010;
+
+ private static final int MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE = 7000;
+
+ private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+ private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher";
+ private static final String HANDLER_THREAD_NAME = "android.imms";
+
+ /**
+ * When set, {@link #startInputUncheckedLocked} will return
+ * {@link InputBindResult#NO_EDITOR} instead of starting an IME connection
+ * unless {@link StartInputFlags#IS_TEXT_EDITOR} is set. This behavior overrides
+ * {@link LayoutParams#SOFT_INPUT_STATE_VISIBLE SOFT_INPUT_STATE_VISIBLE} and
+ * {@link LayoutParams#SOFT_INPUT_STATE_ALWAYS_VISIBLE SOFT_INPUT_STATE_ALWAYS_VISIBLE}
+ * starting from {@link android.os.Build.VERSION_CODES#P}.
+ */
+ private final boolean mPreventImeStartupUnlessTextEditor;
+
+ /**
+ * These IMEs are known not to behave well when evicted from memory and thus are exempt
+ * from the IME startup avoidance behavior that is enabled by
+ * {@link #mPreventImeStartupUnlessTextEditor}.
+ */
+ @NonNull
+ private final String[] mNonPreemptibleInputMethods;
+
+ @UserIdInt
+ private int mLastSwitchUserId;
+
+ final Context mContext;
+ final Resources mRes;
+ private final Handler mHandler;
+ final InputMethodSettings mSettings;
+ final SettingsObserver mSettingsObserver;
+ private final SparseBooleanArray mLoggedDeniedGetInputMethodWindowVisibleHeightForUid =
+ new SparseBooleanArray(0);
+ final WindowManagerInternal mWindowManagerInternal;
+ private final ActivityManagerInternal mActivityManagerInternal;
+ final PackageManagerInternal mPackageManagerInternal;
+ final InputManagerInternal mInputManagerInternal;
+ final ImePlatformCompatUtils mImePlatformCompatUtils;
+ final InputMethodDeviceConfigs mInputMethodDeviceConfigs;
+ private final DisplayManagerInternal mDisplayManagerInternal;
+ private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
+ new ArrayMap<>();
+ private final UserManagerInternal mUserManagerInternal;
+
+ // begin CarInputMethodManagerService
+ private final CarInputMethodMenuController mMenuController;
+ @NonNull private final CarInputMethodBindingController mBindingController;
+ @NonNull private final AutofillController mAutofillController;
+
+ @GuardedBy("ImfLock.class")
+ @NonNull private final CarImeVisibilityStateComputer mVisibilityStateComputer;
+
+ @GuardedBy("ImfLock.class")
+ @NonNull private final CarDefaultImeVisibilityApplier mVisibilityApplier;
+
+ private final LocalServiceImpl mImmi;
+
+ private final ExecutorService mExecutor;
+
+ private ImmsBroadcastReceiverForAllUsers mBroadcastReceiver;
+ // end CarInputMethodManagerService
+
+ /**
+ * Cache the result of {@code LocalServices.getService(AudioManagerInternal.class)}.
+ *
+ * <p>This field is used only within {@link #handleMessage(Message)} hence synchronization is
+ * not necessary.</p>
+ */
+ @Nullable
+ private AudioManagerInternal mAudioManagerInternal = null;
+ @Nullable
+ private VirtualDeviceManagerInternal mVdmInternal = null;
+
+ // All known input methods.
+ final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
+ final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
+ final InputMethodSubtypeSwitchingController mSwitchingController;
+ final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController =
+ new HardwareKeyboardShortcutController();
+
+ /**
+ * Tracks how many times {@link #mMethodMap} was updated.
+ */
+ @GuardedBy("ImfLock.class")
+ private int mMethodMapUpdateCount = 0;
+
+ /**
+ * The display id for which the latest startInput was called.
+ */
+ @GuardedBy("ImfLock.class")
+ int getDisplayIdToShowImeLocked() {
+ return mDisplayIdToShowIme;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private int mDisplayIdToShowIme = INVALID_DISPLAY;
+
+ @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
+ private boolean mShowOngoingImeSwitcherForPhones;
+ @GuardedBy("ImfLock.class")
+ private final HandwritingModeController mHwController;
+ @GuardedBy("ImfLock.class")
+ private IntArray mStylusIds;
+
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private OverlayableSystemBooleanResourceWrapper mImeDrawsImeNavBarRes;
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ Future<?> mImeDrawsImeNavBarResLazyInitFuture;
+
+ static class SessionState {
+ final ClientState mClient;
+ final IInputMethodInvoker mMethod;
+
+ IInputMethodSession mSession;
+ InputChannel mChannel;
+
+ @Override
+ public String toString() {
+ return "SessionState{uid " + mClient.mUid + " pid " + mClient.mPid
+ + " method " + Integer.toHexString(
+ IInputMethodInvoker.getBinderIdentityHashCode(mMethod))
+ + " session " + Integer.toHexString(
+ System.identityHashCode(mSession))
+ + " channel " + mChannel
+ + "}";
+ }
+
+ SessionState(ClientState client, IInputMethodInvoker method,
+ IInputMethodSession session, InputChannel channel) {
+ mClient = client;
+ mMethod = method;
+ mSession = session;
+ mChannel = channel;
+ }
+ }
+
+ /**
+ * Record session state for an accessibility service.
+ */
+ private static class AccessibilitySessionState {
+ final ClientState mClient;
+ // Id of the accessibility service.
+ final int mId;
+
+ public IAccessibilityInputMethodSession mSession;
+
+ @Override
+ public String toString() {
+ return "AccessibilitySessionState{uid " + mClient.mUid + " pid " + mClient.mPid
+ + " id " + Integer.toHexString(mId)
+ + " session " + Integer.toHexString(
+ System.identityHashCode(mSession))
+ + "}";
+ }
+
+ AccessibilitySessionState(ClientState client, int id,
+ IAccessibilityInputMethodSession session) {
+ mClient = client;
+ mId = id;
+ mSession = session;
+ }
+ }
+
+ private static final class ClientDeathRecipient implements IBinder.DeathRecipient {
+ private final CarInputMethodManagerService mImms;
+ private final IInputMethodClient mClient;
+
+ ClientDeathRecipient(CarInputMethodManagerService imms, IInputMethodClient client) {
+ mImms = imms;
+ mClient = client;
+ }
+
+ @Override
+ public void binderDied() {
+ mImms.removeClient(mClient);
+ }
+ }
+
+ static final class ClientState {
+ final IInputMethodClientInvoker mClient;
+ final IRemoteInputConnection mFallbackInputConnection;
+ final int mUid;
+ final int mPid;
+ final int mSelfReportedDisplayId;
+ final InputBinding mBinding;
+ final ClientDeathRecipient mClientDeathRecipient;
+
+ boolean mSessionRequested;
+ boolean mSessionRequestedForAccessibility;
+ SessionState mCurSession;
+ SparseArray<AccessibilitySessionState> mAccessibilitySessions = new SparseArray<>();
+
+ @Override
+ public String toString() {
+ return "ClientState{" + Integer.toHexString(
+ System.identityHashCode(this)) + " mUid=" + mUid
+ + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}";
+ }
+
+ ClientState(IInputMethodClientInvoker client,
+ IRemoteInputConnection fallbackInputConnection,
+ int uid, int pid, int selfReportedDisplayId,
+ ClientDeathRecipient clientDeathRecipient) {
+ mClient = client;
+ mFallbackInputConnection = fallbackInputConnection;
+ mUid = uid;
+ mPid = pid;
+ mSelfReportedDisplayId = selfReportedDisplayId;
+ mBinding = new InputBinding(null, mFallbackInputConnection.asBinder(), mUid, mPid);
+ mClientDeathRecipient = clientDeathRecipient;
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+
+ private static final class VirtualDisplayInfo {
+ /**
+ * {@link ClientState} where {@link android.hardware.display.VirtualDisplay} is running.
+ */
+ private final ClientState mParentClient;
+ /**
+ * {@link Matrix} to convert screen coordinates in the embedded virtual display to
+ * screen coordinates where {@link #mParentClient} exists.
+ */
+ private final Matrix mMatrix;
+
+ VirtualDisplayInfo(ClientState parentClient, Matrix matrix) {
+ mParentClient = parentClient;
+ mMatrix = matrix;
+ }
+ }
+
+ /**
+ * A mapping table from virtual display IDs created for
+ * {@link android.hardware.display.VirtualDisplay} to its parent IME client where the embedded
+ * virtual display is running.
+ *
+ * <p>Note: this can be used only for virtual display IDs created by
+ * {@link android.hardware.display.VirtualDisplay}.</p>
+ */
+ @GuardedBy("ImfLock.class")
+ private final SparseArray<VirtualDisplayInfo> mVirtualDisplayIdToParentMap =
+ new SparseArray<>();
+
+ /**
+ * Set once the system is ready to run third party code.
+ */
+ boolean mSystemReady;
+
+ /**
+ * Id obtained with {@link InputMethodInfo#getId()} for the currently selected input method.
+ * This is to be synchronized with the secure settings keyed with
+ * {@link Settings.Secure#DEFAULT_INPUT_METHOD}.
+ *
+ * <p>This can be transiently {@code null} when the system is re-initializing input method
+ * settings, e.g., the system locale is just changed.</p>
+ *
+ * <p>Note that {@link InputMethodBindingController#getCurId()} is used to track which IME
+ * is being connected to {@link CarInputMethodManagerService}.</p>
+ *
+ * @see InputMethodBindingController#getCurId()
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ String getSelectedMethodIdLocked() {
+ return mBindingController.getSelectedMethodId();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void setSelectedMethodIdLocked(@Nullable String selectedMethodId) {
+ mBindingController.setSelectedMethodId(selectedMethodId);
+ }
+
+ /**
+ * The current binding sequence number, incremented every time there is
+ * a new bind performed.
+ */
+ @GuardedBy("ImfLock.class")
+ private int getSequenceNumberLocked() {
+ return mBindingController.getSequenceNumber();
+ }
+
+ /**
+ * Increase the current binding sequence number by one.
+ * Reset to 1 on overflow.
+ */
+ @GuardedBy("ImfLock.class")
+ private void advanceSequenceNumberLocked() {
+ mBindingController.advanceSequenceNumber();
+ }
+
+ /**
+ * The client that is currently bound to an input method.
+ */
+ @Nullable
+ private ClientState mCurClient;
+
+ /**
+ * The last window token that we confirmed to be focused. This is always updated upon reports
+ * from the input method client. If the window state is already changed before the report is
+ * handled, this field just keeps the last value.
+ */
+ IBinder mCurFocusedWindow;
+
+ /**
+ * The last window token that we confirmed that IME started talking to. This is always updated
+ * upon reports from the input method. If the window state is already changed before the report
+ * is handled, this field just keeps the last value.
+ */
+ IBinder mLastImeTargetWindow;
+
+ /**
+ * {@link LayoutParams#softInputMode} of {@link #mCurFocusedWindow}.
+ *
+ * @see #mCurFocusedWindow
+ */
+ @SoftInputModeFlags
+ int mCurFocusedWindowSoftInputMode;
+
+ /**
+ * The client by which {@link #mCurFocusedWindow} was reported. This gets updated whenever an
+ * IME-focusable window gained focus (without necessarily starting an input connection),
+ * while {@link #mCurClient} only gets updated when we actually start an input connection.
+ *
+ * @see #mCurFocusedWindow
+ */
+ @Nullable
+ ClientState mCurFocusedWindowClient;
+
+ /**
+ * The editor info by which {@link #mCurFocusedWindow} was reported. This differs from
+ * {@link #mCurEditorInfo} the same way {@link #mCurFocusedWindowClient} differs
+ * from {@link #mCurClient}.
+ *
+ * @see #mCurFocusedWindow
+ */
+ @Nullable
+ EditorInfo mCurFocusedWindowEditorInfo;
+
+ /**
+ * The {@link IRemoteInputConnection} last provided by the current client.
+ */
+ IRemoteInputConnection mCurInputConnection;
+
+ /**
+ * The {@link ImeOnBackInvokedDispatcher} last provided by the current client to
+ * receive {@link android.window.OnBackInvokedCallback}s forwarded from IME.
+ */
+ ImeOnBackInvokedDispatcher mCurImeDispatcher;
+
+ /**
+ * The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
+ */
+ @Nullable IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
+
+ /**
+ * The {@link EditorInfo} last provided by the current client.
+ */
+ @Nullable
+ EditorInfo mCurEditorInfo;
+
+ /**
+ * A special {@link Matrix} to convert virtual screen coordinates to the IME target display
+ * coordinates.
+ *
+ * <p>Used only while the IME client is running in a virtual display. {@code null}
+ * otherwise.</p>
+ */
+ @Nullable
+ private Matrix mCurVirtualDisplayToScreenMatrix = null;
+
+ /**
+ * Id obtained with {@link InputMethodInfo#getId()} for the input method that we are currently
+ * connected to or in the process of connecting to.
+ *
+ * <p>This can be {@code null} when no input method is connected.</p>
+ *
+ * @see #getSelectedMethodIdLocked()
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private String getCurIdLocked() {
+ return mBindingController.getCurId();
+ }
+
+ /**
+ * The current subtype of the current input method.
+ */
+ private InputMethodSubtype mCurrentSubtype;
+
+ /**
+ * {@code true} if the IME has not been mostly hidden via {@link android.view.InsetsController}
+ */
+ private boolean mCurPerceptible;
+
+ /**
+ * Set to true if our ServiceConnection is currently actively bound to
+ * a service (whether or not we have gotten its IBinder back yet).
+ */
+ @GuardedBy("ImfLock.class")
+ private boolean hasConnectionLocked() {
+ return mBindingController.hasConnection();
+ }
+
+ /** The token tracking the current IME request or {@code null} otherwise. */
+ @Nullable
+ private ImeTracker.Token mCurStatsToken;
+
+ /**
+ * {@code true} if the current input method is in fullscreen mode.
+ */
+ boolean mInFullscreenMode;
+
+ /**
+ * The Intent used to connect to the current input method.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private Intent getCurIntentLocked() {
+ return mBindingController.getCurIntent();
+ }
+
+ /**
+ * The token we have made for the currently active input method, to
+ * identify it in the future.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ IBinder getCurTokenLocked() {
+ return mBindingController.getCurToken();
+ }
+
+ /**
+ * The displayId of current active input method.
+ */
+ @GuardedBy("ImfLock.class")
+ int getCurTokenDisplayIdLocked() {
+ return mCurTokenDisplayId;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void setCurTokenDisplayIdLocked(int curTokenDisplayId) {
+ mCurTokenDisplayId = curTokenDisplayId;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private int mCurTokenDisplayId = INVALID_DISPLAY;
+
+ /**
+ * The host input token of the current active input method.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private IBinder mCurHostInputToken;
+
+ /**
+ * The display ID of the input method indicates the fallback display which returned by
+ * {@link #computeImeDisplayIdForTarget}.
+ */
+ static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY;
+
+ /**
+ * If non-null, this is the input method service we are currently connected
+ * to.
+ */
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ IInputMethodInvoker getCurMethodLocked() {
+ return mBindingController.getCurMethod();
+ }
+
+ /**
+ * If not {@link Process#INVALID_UID}, then the UID of {@link #getCurIntentLocked()}.
+ */
+ @GuardedBy("ImfLock.class")
+ private int getCurMethodUidLocked() {
+ return mBindingController.getCurMethodUid();
+ }
+
+ /**
+ * Time that we last initiated a bind to the input method, to determine
+ * if we should try to disconnect and reconnect to it.
+ */
+ @GuardedBy("ImfLock.class")
+ private long getLastBindTimeLocked() {
+ return mBindingController.getLastBindTime();
+ }
+
+ /**
+ * Have we called mCurMethod.bindInput()?
+ */
+ boolean mBoundToMethod;
+
+ /**
+ * Have we called bindInput() for accessibility services?
+ */
+ boolean mBoundToAccessibility;
+
+ /**
+ * Currently enabled session.
+ */
+ @GuardedBy("ImfLock.class")
+ SessionState mEnabledSession;
+ SparseArray<AccessibilitySessionState> mEnabledAccessibilitySessions = new SparseArray<>();
+
+ /**
+ * True if the device is currently interactive with user. The value is true initially.
+ */
+ boolean mIsInteractive = true;
+
+ int mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT;
+
+ /**
+ * A set of status bits regarding the active IME.
+ *
+ * <p>This value is a combination of following two bits:</p>
+ * <dl>
+ * <dt>{@link InputMethodService#IME_ACTIVE}</dt>
+ * <dd>
+ * If this bit is ON, connected IME is ready to accept touch/key events.
+ * </dd>
+ * <dt>{@link InputMethodService#IME_VISIBLE}</dt>
+ * <dd>
+ * If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
+ * </dd>
+ * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
+ * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
+ * currently invisible.
+ * </dd>
+ * </dl>
+ * <em>Do not update this value outside of {@link #setImeWindowStatus(IBinder, int, int)} and
+ * {@link InputMethodBindingController#unbindCurrentMethod()}.</em>
+ */
+ int mImeWindowVis;
+
+ private LocaleList mLastSystemLocales;
+ private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor();
+ private final String mSlotIme;
+
+ /**
+ * Registered {@link InputMethodListListener}.
+ * This variable can be accessed from both of MainThread and BinderThread.
+ */
+ private final CopyOnWriteArrayList<InputMethodListListener> mInputMethodListListeners =
+ new CopyOnWriteArrayList<>();
+
+ /**
+ * Internal state snapshot when
+ * {@link IInputMethod#startInput(IInputMethod.StartInputParams)} is about to be called.
+ *
+ * <p>Calling that IPC endpoint basically means that
+ * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called
+ * back in the current IME process shortly, which will also affect what the current IME starts
+ * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this
+ * snapshot will be taken every time when {@link CarInputMethodManagerService} is initiating a
+ * new logical input session between the client application and the current IME.</p>
+ *
+ * <p>Be careful to not keep strong references to this object forever, which can prevent
+ * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed.
+ * </p>
+ */
+ private static class StartInputInfo {
+ private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+ final int mSequenceNumber;
+ final long mTimestamp;
+ final long mWallTime;
+ @UserIdInt
+ final int mImeUserId;
+ @NonNull
+ final IBinder mImeToken;
+ final int mImeDisplayId;
+ @NonNull
+ final String mImeId;
+ @StartInputReason
+ final int mStartInputReason;
+ final boolean mRestarting;
+ @UserIdInt
+ final int mTargetUserId;
+ final int mTargetDisplayId;
+ @Nullable
+ final IBinder mTargetWindow;
+ @NonNull
+ final EditorInfo mEditorInfo;
+ @SoftInputModeFlags
+ final int mTargetWindowSoftInputMode;
+ final int mClientBindSequenceNumber;
+
+ StartInputInfo(@UserIdInt int imeUserId, @NonNull IBinder imeToken, int imeDisplayId,
+ @NonNull String imeId, @StartInputReason int startInputReason, boolean restarting,
+ @UserIdInt int targetUserId, int targetDisplayId, @Nullable IBinder targetWindow,
+ @NonNull EditorInfo editorInfo, @SoftInputModeFlags int targetWindowSoftInputMode,
+ int clientBindSequenceNumber) {
+ mSequenceNumber = sSequenceNumber.getAndIncrement();
+ mTimestamp = SystemClock.uptimeMillis();
+ mWallTime = System.currentTimeMillis();
+ mImeUserId = imeUserId;
+ mImeToken = imeToken;
+ mImeDisplayId = imeDisplayId;
+ mImeId = imeId;
+ mStartInputReason = startInputReason;
+ mRestarting = restarting;
+ mTargetUserId = targetUserId;
+ mTargetDisplayId = targetDisplayId;
+ mTargetWindow = targetWindow;
+ mEditorInfo = editorInfo;
+ mTargetWindowSoftInputMode = targetWindowSoftInputMode;
+ mClientBindSequenceNumber = clientBindSequenceNumber;
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private final WeakHashMap<IBinder, IBinder> mImeTargetWindowMap = new WeakHashMap<>();
+
+ @VisibleForTesting
+ static final class SoftInputShowHideHistory {
+ private final Entry[] mEntries = new Entry[16];
+ private int mNextIndex = 0;
+ private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
+
+ static final class Entry {
+ final int mSequenceNumber = sSequenceNumber.getAndIncrement();
+ @Nullable
+ final ClientState mClientState;
+ @SoftInputModeFlags
+ final int mFocusedWindowSoftInputMode;
+ @SoftInputShowHideReason
+ final int mReason;
+ // The timing of handling showCurrentInputLocked() or hideCurrentInputLocked().
+ final long mTimestamp;
+ final long mWallTime;
+ final boolean mInFullscreenMode;
+ @NonNull
+ final String mFocusedWindowName;
+ @Nullable
+ final EditorInfo mEditorInfo;
+ @NonNull
+ final String mRequestWindowName;
+ @Nullable
+ final String mImeControlTargetName;
+ @Nullable
+ final String mImeTargetNameFromWm;
+ @Nullable
+ final String mImeSurfaceParentName;
+
+ Entry(ClientState client, EditorInfo editorInfo, String focusedWindowName,
+ @SoftInputModeFlags int softInputMode, @SoftInputShowHideReason int reason,
+ boolean inFullscreenMode, String requestWindowName,
+ @Nullable String imeControlTargetName, @Nullable String imeTargetName,
+ @Nullable String imeSurfaceParentName) {
+ mClientState = client;
+ mEditorInfo = editorInfo;
+ mFocusedWindowName = focusedWindowName;
+ mFocusedWindowSoftInputMode = softInputMode;
+ mReason = reason;
+ mTimestamp = SystemClock.uptimeMillis();
+ mWallTime = System.currentTimeMillis();
+ mInFullscreenMode = inFullscreenMode;
+ mRequestWindowName = requestWindowName;
+ mImeControlTargetName = imeControlTargetName;
+ mImeTargetNameFromWm = imeTargetName;
+ mImeSurfaceParentName = imeSurfaceParentName;
+ }
+ }
+
+ void addEntry(@NonNull Entry entry) {
+ final int index = mNextIndex;
+ mEntries[index] = entry;
+ mNextIndex = (mNextIndex + 1) % mEntries.length;
+ }
+
+ void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ .withZone(ZoneId.systemDefault());
+
+ for (int i = 0; i < mEntries.length; ++i) {
+ final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
+ if (entry == null) {
+ continue;
+ }
+ pw.print(prefix);
+ pw.println("SoftInputShowHideHistory #" + entry.mSequenceNumber + ":");
+
+ pw.print(prefix);
+ pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+ + " (timestamp=" + entry.mTimestamp + ")");
+
+ pw.print(prefix);
+ pw.print(" reason=" + InputMethodDebug.softInputDisplayReasonToString(
+ entry.mReason));
+ pw.println(" inFullscreenMode=" + entry.mInFullscreenMode);
+
+ pw.print(prefix);
+ pw.println(" requestClient=" + entry.mClientState);
+
+ pw.print(prefix);
+ pw.println(" focusedWindowName=" + entry.mFocusedWindowName);
+
+ pw.print(prefix);
+ pw.println(" requestWindowName=" + entry.mRequestWindowName);
+
+ pw.print(prefix);
+ pw.println(" imeControlTargetName=" + entry.mImeControlTargetName);
+
+ pw.print(prefix);
+ pw.println(" imeTargetNameFromWm=" + entry.mImeTargetNameFromWm);
+
+ pw.print(prefix);
+ pw.println(" imeSurfaceParentName=" + entry.mImeSurfaceParentName);
+
+ pw.print(prefix);
+ pw.print(" editorInfo: ");
+ if (entry.mEditorInfo != null) {
+ pw.print(" inputType=" + entry.mEditorInfo.inputType);
+ pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions);
+ pw.println(" fieldId (viewId)=" + entry.mEditorInfo.fieldId);
+ } else {
+ pw.println("null");
+ }
+
+ pw.print(prefix);
+ pw.println(" focusedWindowSoftInputMode=" + InputMethodDebug.softInputModeToString(
+ entry.mFocusedWindowSoftInputMode));
+ }
+ }
+ }
+
+ /**
+ * A ring buffer to store the history of {@link StartInputInfo}.
+ */
+ private static final class StartInputHistory {
+ /**
+ * Entry size for non low-RAM devices.
+ *
+ * <p>TODO: Consider to follow what other system services have been doing to manage
+ * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+ */
+ private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32;
+
+ /**
+ * Entry size for low-RAM devices.
+ *
+ * <p>TODO: Consider to follow what other system services have been doing to manage
+ * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).</p>
+ */
+ private static final int ENTRY_SIZE_FOR_LOW_RAM_DEVICE = 5;
+
+ private static int getEntrySize() {
+ if (ActivityManager.isLowRamDeviceStatic()) {
+ return ENTRY_SIZE_FOR_LOW_RAM_DEVICE;
+ } else {
+ return ENTRY_SIZE_FOR_HIGH_RAM_DEVICE;
+ }
+ }
+
+ /**
+ * Backing store for the ring buffer.
+ */
+ private final Entry[] mEntries = new Entry[getEntrySize()];
+
+ /**
+ * An index of {@link #mEntries}, to which next {@link #addEntry(StartInputInfo)} should
+ * write.
+ */
+ private int mNextIndex = 0;
+
+ /**
+ * Recyclable entry to store the information in {@link StartInputInfo}.
+ */
+ private static final class Entry {
+ int mSequenceNumber;
+ long mTimestamp;
+ long mWallTime;
+ @UserIdInt
+ int mImeUserId;
+ @NonNull
+ String mImeTokenString;
+ int mImeDisplayId;
+ @NonNull
+ String mImeId;
+ @StartInputReason
+ int mStartInputReason;
+ boolean mRestarting;
+ @UserIdInt
+ int mTargetUserId;
+ int mTargetDisplayId;
+ @NonNull
+ String mTargetWindowString;
+ @NonNull
+ EditorInfo mEditorInfo;
+ @SoftInputModeFlags
+ int mTargetWindowSoftInputMode;
+ int mClientBindSequenceNumber;
+
+ Entry(@NonNull StartInputInfo original) {
+ set(original);
+ }
+
+ void set(@NonNull StartInputInfo original) {
+ mSequenceNumber = original.mSequenceNumber;
+ mTimestamp = original.mTimestamp;
+ mWallTime = original.mWallTime;
+ mImeUserId = original.mImeUserId;
+ // Intentionally convert to String so as not to keep a strong reference to a Binder
+ // object.
+ mImeTokenString = String.valueOf(original.mImeToken);
+ mImeDisplayId = original.mImeDisplayId;
+ mImeId = original.mImeId;
+ mStartInputReason = original.mStartInputReason;
+ mRestarting = original.mRestarting;
+ mTargetUserId = original.mTargetUserId;
+ mTargetDisplayId = original.mTargetDisplayId;
+ // Intentionally convert to String so as not to keep a strong reference to a Binder
+ // object.
+ mTargetWindowString = String.valueOf(original.mTargetWindow);
+ mEditorInfo = original.mEditorInfo;
+ mTargetWindowSoftInputMode = original.mTargetWindowSoftInputMode;
+ mClientBindSequenceNumber = original.mClientBindSequenceNumber;
+ }
+ }
+
+ /**
+ * Add a new entry and discard the oldest entry as needed.
+ * @param info {@link StartInputInfo} to be added.
+ */
+ void addEntry(@NonNull StartInputInfo info) {
+ final int index = mNextIndex;
+ if (mEntries[index] == null) {
+ mEntries[index] = new Entry(info);
+ } else {
+ mEntries[index].set(info);
+ }
+ mNextIndex = (mNextIndex + 1) % mEntries.length;
+ }
+
+ void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ final DateTimeFormatter formatter =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+ .withZone(ZoneId.systemDefault());
+
+ for (int i = 0; i < mEntries.length; ++i) {
+ final Entry entry = mEntries[(i + mNextIndex) % mEntries.length];
+ if (entry == null) {
+ continue;
+ }
+ pw.print(prefix);
+ pw.println("StartInput #" + entry.mSequenceNumber + ":");
+
+ pw.print(prefix);
+ pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
+ + " (timestamp=" + entry.mTimestamp + ")"
+ + " reason="
+ + InputMethodDebug.startInputReasonToString(entry.mStartInputReason)
+ + " restarting=" + entry.mRestarting);
+
+ pw.print(prefix);
+ pw.print(" imeToken=" + entry.mImeTokenString + " [" + entry.mImeId + "]");
+ pw.print(" imeUserId=" + entry.mImeUserId);
+ pw.println(" imeDisplayId=" + entry.mImeDisplayId);
+
+ pw.print(prefix);
+ pw.println(" targetWin=" + entry.mTargetWindowString
+ + " [" + entry.mEditorInfo.packageName + "]"
+ + " targetUserId=" + entry.mTargetUserId
+ + " targetDisplayId=" + entry.mTargetDisplayId
+ + " clientBindSeq=" + entry.mClientBindSequenceNumber);
+
+ pw.print(prefix);
+ pw.println(" softInputMode=" + InputMethodDebug.softInputModeToString(
+ entry.mTargetWindowSoftInputMode));
+
+ pw.print(prefix);
+ pw.println(" inputType=0x" + Integer.toHexString(entry.mEditorInfo.inputType)
+ + " imeOptions=0x" + Integer.toHexString(entry.mEditorInfo.imeOptions)
+ + " fieldId=0x" + Integer.toHexString(entry.mEditorInfo.fieldId)
+ + " fieldName=" + entry.mEditorInfo.fieldName
+ + " actionId=" + entry.mEditorInfo.actionId
+ + " actionLabel=" + entry.mEditorInfo.actionLabel);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private final StartInputHistory mStartInputHistory = new StartInputHistory();
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private final SoftInputShowHideHistory mSoftInputShowHideHistory =
+ new SoftInputShowHideHistory();
+
+ @NonNull
+ private final ImeTrackerService mImeTrackerService;
+
+ class SettingsObserver extends ContentObserver {
+ int mUserId;
+ boolean mRegistered = false;
+ @NonNull
+ String mLastEnabled = "";
+
+ /**
+ * <em>This constructor must be called within the lock.</em>
+ */
+ SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ @GuardedBy("ImfLock.class")
+ public void registerContentObserverLocked(@UserIdInt int userId) {
+ if (mRegistered && mUserId == userId) {
+ return;
+ }
+ ContentResolver resolver = mContext.getContentResolver();
+ if (mRegistered) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ mRegistered = false;
+ }
+ if (mUserId != userId) {
+ mLastEnabled = "";
+ mUserId = userId;
+ }
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.DEFAULT_INPUT_METHOD), false, this, userId);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.ENABLED_INPUT_METHODS), false, this, userId);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE), false, this, userId);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD), false, this, userId);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE), false, this, userId);
+ mRegistered = true;
+ }
+
+ @Override public void onChange(boolean selfChange, Uri uri) {
+ final Uri showImeUri = Settings.Secure.getUriFor(
+ Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD);
+ final Uri accessibilityRequestingNoImeUri = Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE);
+ synchronized (ImfLock.class) {
+ if (showImeUri.equals(uri)) {
+ mMenuController.updateKeyboardFromSettingsLocked();
+ } else if (accessibilityRequestingNoImeUri.equals(uri)) {
+ final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId);
+ mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard(
+ accessibilitySoftKeyboardSetting);
+ if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE);
+ } else if (isShowRequestedForCurrentWindow()) {
+ showCurrentInputImplicitLocked(mCurFocusedWindow,
+ SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE);
+ }
+ } else {
+ boolean enabledChanged = false;
+ String newEnabled = mSettings.getEnabledInputMethodsStr();
+ if (!mLastEnabled.equals(newEnabled)) {
+ mLastEnabled = newEnabled;
+ enabledChanged = true;
+ }
+ updateInputMethodsFromSettingsLocked(enabledChanged);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SettingsObserver{mUserId=" + mUserId + " mRegistered=" + mRegistered
+ + " mLastEnabled=" + mLastEnabled + "}";
+ }
+ }
+
+ /**
+ * {@link BroadcastReceiver} that is intended to listen to broadcasts sent to all the users.
+ */
+ private final class ImmsBroadcastReceiverForAllUsers extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+ final PendingResult pendingResult = getPendingResult();
+ if (pendingResult == null) {
+ return;
+ }
+ // sender userId can be a real user ID or USER_ALL.
+ final int senderUserId = pendingResult.getSendingUserId();
+ if (senderUserId != UserHandle.USER_ALL) {
+ if (senderUserId != mSettings.getCurrentUserId()) {
+ // A background user is trying to hide the dialog. Ignore.
+ return;
+ }
+ }
+ mMenuController.hideInputMethodMenu();
+ } else {
+ Slog.w(TAG, "Unexpected intent " + intent);
+ }
+ }
+ }
+
+ /**
+ * Handles {@link Intent#ACTION_LOCALE_CHANGED}.
+ *
+ * <p>Note: For historical reasons, {@link Intent#ACTION_LOCALE_CHANGED} has been sent to all
+ * the users. We should ignore this event if this is about any background user's locale.</p>
+ *
+ * <p>Caution: This method must not be called when system is not ready.</p>
+ */
+ void onActionLocaleChanged() {
+ synchronized (ImfLock.class) {
+ final LocaleList possibleNewLocale = mRes.getConfiguration().getLocales();
+ if (possibleNewLocale != null && possibleNewLocale.equals(mLastSystemLocales)) {
+ return;
+ }
+ buildInputMethodListLocked(true);
+ // If the locale is changed, needs to reset the default ime
+ resetDefaultImeLocked(mContext);
+ updateFromSettingsLocked(true);
+ mLastSystemLocales = possibleNewLocale;
+ }
+ }
+
+ final class MyPackageMonitor extends PackageMonitor {
+ /**
+ * Package names that are known to contain {@link InputMethodService}.
+ *
+ * <p>No need to include packages because of direct-boot unaware IMEs since we always rescan
+ * all the packages when the user is unlocked, and direct-boot awareness will not be changed
+ * dynamically unless the entire package is updated, which also always triggers package
+ * rescanning.</p>
+ */
+ @GuardedBy("ImfLock.class")
+ private final ArraySet<String> mKnownImePackageNames = new ArraySet<>();
+
+ /**
+ * Packages that are appeared, disappeared, or modified for whatever reason.
+ *
+ * <p>Note: For now we intentionally use {@link ArrayList} instead of {@link ArraySet}
+ * because 1) the number of elements is almost always 1 or so, and 2) we do not care
+ * duplicate elements for our use case.</p>
+ *
+ * <p>This object must be accessed only from callback methods in {@link PackageMonitor},
+ * which should be bound to {@link #getRegisteredHandler()}.</p>
+ */
+ private final ArrayList<String> mChangedPackages = new ArrayList<>();
+
+ /**
+ * {@code true} if one or more packages that contain {@link InputMethodService} appeared.
+ *
+ * <p>This field must be accessed only from callback methods in {@link PackageMonitor},
+ * which should be bound to {@link #getRegisteredHandler()}.</p>
+ */
+ private boolean mImePackageAppeared = false;
+
+ @GuardedBy("ImfLock.class")
+ void clearKnownImePackageNamesLocked() {
+ mKnownImePackageNames.clear();
+ }
+
+ @GuardedBy("ImfLock.class")
+ void addKnownImePackageNameLocked(@NonNull String packageName) {
+ mKnownImePackageNames.add(packageName);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isChangingPackagesOfCurrentUserLocked() {
+ final int userId = getChangingUserId();
+ final boolean retval = userId == mSettings.getCurrentUserId();
+ if (DEBUG) {
+ if (!retval) {
+ Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
+ synchronized (ImfLock.class) {
+ if (!isChangingPackagesOfCurrentUserLocked()) {
+ return false;
+ }
+ String curInputMethodId = mSettings.getSelectedInputMethod();
+ final int numImes = mMethodList.size();
+ if (curInputMethodId != null) {
+ for (int i = 0; i < numImes; i++) {
+ InputMethodInfo imi = mMethodList.get(i);
+ if (imi.getId().equals(curInputMethodId)) {
+ for (String pkg : packages) {
+ if (imi.getPackageName().equals(pkg)) {
+ if (!doit) {
+ return true;
+ }
+ resetSelectedInputMethodAndSubtypeLocked("");
+ chooseNewDefaultIMELocked();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onBeginPackageChanges() {
+ clearPackageChangeState();
+ }
+
+ @Override
+ public void onPackageAppeared(String packageName, int reason) {
+ if (!mImePackageAppeared) {
+ final PackageManager pm = mContext.getPackageManager();
+ final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
+ new Intent(InputMethod.SERVICE_INTERFACE).setPackage(packageName),
+ PackageManager.MATCH_DISABLED_COMPONENTS, getChangingUserId());
+ // No need to lock this because we access it only on getRegisteredHandler().
+ if (!services.isEmpty()) {
+ mImePackageAppeared = true;
+ }
+ }
+ // No need to lock this because we access it only on getRegisteredHandler().
+ mChangedPackages.add(packageName);
+ }
+
+ @Override
+ public void onPackageDisappeared(String packageName, int reason) {
+ // No need to lock this because we access it only on getRegisteredHandler().
+ mChangedPackages.add(packageName);
+ }
+
+ @Override
+ public void onPackageModified(String packageName) {
+ // No need to lock this because we access it only on getRegisteredHandler().
+ mChangedPackages.add(packageName);
+ }
+
+ @Override
+ public void onPackagesSuspended(String[] packages) {
+ // No need to lock this because we access it only on getRegisteredHandler().
+ for (String packageName : packages) {
+ mChangedPackages.add(packageName);
+ }
+ }
+
+ @Override
+ public void onPackagesUnsuspended(String[] packages) {
+ // No need to lock this because we access it only on getRegisteredHandler().
+ for (String packageName : packages) {
+ mChangedPackages.add(packageName);
+ }
+ }
+
+ @Override
+ public void onPackageDataCleared(String packageName, int uid) {
+ boolean changed = false;
+ for (InputMethodInfo imi : mMethodList) {
+ if (imi.getPackageName().equals(packageName)) {
+ mAdditionalSubtypeMap.remove(imi.getId());
+ changed = true;
+ }
+ }
+ if (changed) {
+ AdditionalSubtypeUtils.save(
+ mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId());
+ mChangedPackages.add(packageName);
+ }
+ }
+
+ @Override
+ public void onFinishPackageChanges() {
+ onFinishPackageChangesInternal();
+ clearPackageChangeState();
+ }
+
+ @Override
+ public void onUidRemoved(int uid) {
+ synchronized (ImfLock.class) {
+ mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.delete(uid);
+ }
+ }
+
+ private void clearPackageChangeState() {
+ // No need to lock them because we access these fields only on getRegisteredHandler().
+ mChangedPackages.clear();
+ mImePackageAppeared = false;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean shouldRebuildInputMethodListLocked() {
+ // This method is guaranteed to be called only by getRegisteredHandler().
+
+ // If there is any new package that contains at least one IME, then rebuilt the list
+ // of IMEs.
+ if (mImePackageAppeared) {
+ return true;
+ }
+
+ // Otherwise, check if mKnownImePackageNames and mChangedPackages have any intersection.
+ // TODO: Consider to create a utility method to do the following test. List.retainAll()
+ // is an option, but it may still do some extra operations that we do not need here.
+ final int numPackages = mChangedPackages.size();
+ for (int i = 0; i < numPackages; ++i) {
+ final String packageName = mChangedPackages.get(i);
+ if (mKnownImePackageNames.contains(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void onFinishPackageChangesInternal() {
+ synchronized (ImfLock.class) {
+ if (!isChangingPackagesOfCurrentUserLocked()) {
+ return;
+ }
+ if (!shouldRebuildInputMethodListLocked()) {
+ return;
+ }
+
+ InputMethodInfo curIm = null;
+ String curInputMethodId = mSettings.getSelectedInputMethod();
+ final int numImes = mMethodList.size();
+ if (curInputMethodId != null) {
+ for (int i = 0; i < numImes; i++) {
+ InputMethodInfo imi = mMethodList.get(i);
+ final String imiId = imi.getId();
+ if (imiId.equals(curInputMethodId)) {
+ curIm = imi;
+ }
+
+ int change = isPackageDisappearing(imi.getPackageName());
+ if (change == PACKAGE_TEMPORARY_CHANGE
+ || change == PACKAGE_PERMANENT_CHANGE) {
+ Slog.i(TAG, "Input method uninstalled, disabling: "
+ + imi.getComponent());
+ setInputMethodEnabledLocked(imi.getId(), false);
+ } else if (change == PACKAGE_UPDATING) {
+ Slog.i(TAG,
+ "Input method reinstalling, clearing additional subtypes: "
+ + imi.getComponent());
+ mAdditionalSubtypeMap.remove(imi.getId());
+ AdditionalSubtypeUtils.save(mAdditionalSubtypeMap,
+ mMethodMap,
+ mSettings.getCurrentUserId());
+ }
+ }
+ }
+
+ buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
+
+ boolean changed = false;
+
+ if (curIm != null) {
+ int change = isPackageDisappearing(curIm.getPackageName());
+ if (change == PACKAGE_TEMPORARY_CHANGE
+ || change == PACKAGE_PERMANENT_CHANGE) {
+ final PackageManager userAwarePackageManager =
+ getPackageManagerForUser(mContext, mSettings.getCurrentUserId());
+ ServiceInfo si = null;
+ try {
+ si = userAwarePackageManager.getServiceInfo(curIm.getComponent(),
+ PackageManager.ComponentInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ if (si == null) {
+ // Uh oh, current input method is no longer around!
+ // Pick another one...
+ Slog.i(TAG, "Current input method removed: " + curInputMethodId);
+ updateSystemUiLocked(0 /* vis */, mBackDisposition);
+ if (!chooseNewDefaultIMELocked()) {
+ changed = true;
+ curIm = null;
+ Slog.i(TAG, "Unsetting current input method");
+ resetSelectedInputMethodAndSubtypeLocked("");
+ }
+ }
+ }
+ }
+
+ if (curIm == null) {
+ // We currently don't have a default input method... is
+ // one now available?
+ changed = chooseNewDefaultIMELocked();
+ } else if (!changed && isPackageModified(curIm.getPackageName())) {
+ // Even if the current input method is still available, mCurrentSubtype could
+ // be obsolete when the package is modified in practice.
+ changed = true;
+ }
+
+ if (changed) {
+ updateFromSettingsLocked(false);
+ }
+ }
+ }
+ }
+
+ private static final class UserSwitchHandlerTask implements Runnable {
+ final CarInputMethodManagerService mService;
+
+ @UserIdInt
+ final int mToUserId;
+
+ @Nullable
+ IInputMethodClientInvoker mClientToBeReset;
+
+ UserSwitchHandlerTask(CarInputMethodManagerService service, @UserIdInt int toUserId,
+ @Nullable IInputMethodClientInvoker clientToBeReset) {
+ mService = service;
+ mToUserId = toUserId;
+ mClientToBeReset = clientToBeReset;
+ }
+
+ @Override
+ public void run() {
+ synchronized (ImfLock.class) {
+ if (mService.mUserSwitchHandlerTask != this) {
+ // This task was already canceled before it is handled here. So do nothing.
+ return;
+ }
+ mService.switchUserOnHandlerLocked(mService.mUserSwitchHandlerTask.mToUserId,
+ mClientToBeReset);
+ mService.mUserSwitchHandlerTask = null;
+ }
+ }
+ }
+
+ /**
+ * When non-{@code null}, this represents pending user-switch task, which is to be executed as
+ * a handler callback. This needs to be set and unset only within the lock.
+ */
+ @Nullable
+ @GuardedBy("ImfLock.class")
+ private UserSwitchHandlerTask mUserSwitchHandlerTask;
+
+ void onUnlockUser(@UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ final int currentUserId = mSettings.getCurrentUserId();
+ if (DEBUG) {
+ Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId);
+ }
+ if (userId != currentUserId) {
+ return;
+ }
+ mSettings.switchCurrentUser(currentUserId, !mSystemReady);
+ if (mSystemReady) {
+ // We need to rebuild IMEs.
+ buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
+ updateInputMethodsFromSettingsLocked(true /* enabledChanged */);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void scheduleSwitchUserTaskLocked(@UserIdInt int userId,
+ @Nullable IInputMethodClientInvoker clientToBeReset) {
+ if (mUserSwitchHandlerTask != null) {
+ if (mUserSwitchHandlerTask.mToUserId == userId) {
+ mUserSwitchHandlerTask.mClientToBeReset = clientToBeReset;
+ return;
+ }
+ mHandler.removeCallbacks(mUserSwitchHandlerTask);
+ }
+ // Hide soft input before user switch task since switch task may block main handler a while
+ // and delayed the hideCurrentInputLocked().
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_SWITCH_USER);
+ final UserSwitchHandlerTask task = new UserSwitchHandlerTask(this, userId,
+ clientToBeReset);
+ mUserSwitchHandlerTask = task;
+ mHandler.post(task);
+ }
+
+ public CarInputMethodManagerService(Context context, ExecutorService executor) {
+ this(context, null, null, executor);
+ }
+
+ @VisibleForTesting
+ CarInputMethodManagerService(
+ Context context,
+ @Nullable ServiceThread serviceThreadForTesting,
+ @Nullable CarInputMethodBindingController bindingControllerForTesting,
+ ExecutorService executor) {
+ mContext = context;
+ mRes = context.getResources();
+ // TODO(b/196206770): Disallow I/O on this thread. Currently it's needed for loading
+ // additional subtypes in switchUserOnHandlerLocked().
+ final ServiceThread thread =
+ serviceThreadForTesting != null
+ ? serviceThreadForTesting
+ : new ServiceThread(
+ HANDLER_THREAD_NAME,
+ Process.THREAD_PRIORITY_FOREGROUND,
+ true /* allowIo */);
+ thread.start();
+ mHandler = Handler.createAsync(thread.getLooper(), this);
+ mImeTrackerService = new ImeTrackerService(serviceThreadForTesting != null
+ ? serviceThreadForTesting.getLooper() : Looper.getMainLooper());
+ // Note: SettingsObserver doesn't register observers in its constructor.
+ mSettingsObserver = new SettingsObserver(mHandler);
+ mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+ mImePlatformCompatUtils = new ImePlatformCompatUtils();
+ mInputMethodDeviceConfigs = new InputMethodDeviceConfigs();
+ mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+
+ mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
+
+ mShowOngoingImeSwitcherForPhones = false;
+
+ final int userId = mActivityManagerInternal.getCurrentUserId();
+
+ mLastSwitchUserId = userId;
+
+ // mSettings should be created before buildInputMethodListLocked
+ mSettings = new InputMethodSettings(mContext, mMethodMap, userId, !mSystemReady);
+
+ updateCurrentProfileIds();
+ AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
+ mSwitchingController =
+ InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);
+ mHardwareKeyboardShortcutController.reset(mSettings);
+ mMenuController = new CarInputMethodMenuController(this);
+ mBindingController =
+ bindingControllerForTesting != null
+ ? bindingControllerForTesting
+ : new CarInputMethodBindingController(this);
+
+ // CarInputMethodManagerService
+ mAutofillController = userId == UserHandle.USER_SYSTEM
+ ? new NullAutofillSuggestionsController() :
+ new CarAutofillSuggestionsController(this);
+
+ mVisibilityStateComputer = new CarImeVisibilityStateComputer(this);
+ mVisibilityApplier = new CarDefaultImeVisibilityApplier(this);
+
+ mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
+ com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
+ mNonPreemptibleInputMethods = mRes.getStringArray(
+ com.android.internal.R.array.config_nonPreemptibleInputMethods);
+ mHwController = new HandwritingModeController(thread.getLooper(),
+ new InkWindowInitializer());
+ registerDeviceListenerAndCheckStylusSupport();
+ mImmi = new LocalServiceImpl(); // CarInputMethodManagerService
+ mExecutor = executor; // CarInputMethodManagerService
+ }
+
+ // CarInputMethodManagerService
+ InputMethodManagerInternal getInputMethodManagerInternal() {
+ return mImmi;
+ }
+
+ // CarInputMethodManagerService
+ AutofillController getAutofillController() {
+ return mAutofillController;
+ }
+
+ private final class InkWindowInitializer implements Runnable {
+ public void run() {
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.initInkWindow();
+ }
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void resetDefaultImeLocked(Context context) {
+ // Do not reset the default (current) IME when it is a 3rd-party IME
+ String selectedMethodId = getSelectedMethodIdLocked();
+ if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) {
+ return;
+ }
+ final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes(
+ context, mSettings.getEnabledInputMethodListLocked());
+ if (suitableImes.isEmpty()) {
+ Slog.i(TAG, "No default found");
+ return;
+ }
+ final InputMethodInfo defIm = suitableImes.get(0);
+ if (DEBUG) {
+ Slog.i(TAG, "Default found, using " + defIm.getId());
+ }
+ setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void maybeInitImeNavbarConfigLocked(@UserIdInt int targetUserId) {
+ // Currently, com.android.internal.R.bool.config_imeDrawsImeNavBar is overlaid only for the
+ // profile parent user.
+ // TODO(b/221443458): See if we can make OverlayManager be aware of profile groups.
+ final int profileParentUserId = mUserManagerInternal.getProfileParentId(targetUserId);
+ if (mImeDrawsImeNavBarRes != null
+ && mImeDrawsImeNavBarRes.getUserId() != profileParentUserId) {
+ mImeDrawsImeNavBarRes.close();
+ mImeDrawsImeNavBarRes = null;
+ }
+ if (mImeDrawsImeNavBarRes == null) {
+ final Context userContext;
+ if (mContext.getUserId() == profileParentUserId) {
+ userContext = mContext;
+ } else {
+ userContext = mContext.createContextAsUser(UserHandle.of(profileParentUserId),
+ 0 /* flags */);
+ }
+ mImeDrawsImeNavBarRes = OverlayableSystemBooleanResourceWrapper.create(userContext,
+ com.android.internal.R.bool.config_imeDrawsImeNavBar, mHandler, resource -> {
+ synchronized (ImfLock.class) {
+ if (resource == mImeDrawsImeNavBarRes) {
+ sendOnNavButtonFlagsChangedLocked();
+ }
+ }
+ });
+ }
+ }
+
+ @NonNull
+ private static PackageManager getPackageManagerForUser(@NonNull Context context,
+ @UserIdInt int userId) {
+ return context.getUserId() == userId
+ ? context.getPackageManager()
+ : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */)
+ .getPackageManager();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void switchUserOnHandlerLocked(@UserIdInt int newUserId,
+ IInputMethodClientInvoker clientToBeReset) {
+ if (DEBUG) {
+ Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId
+ + " currentUserId=" + mSettings.getCurrentUserId());
+ }
+
+ maybeInitImeNavbarConfigLocked(newUserId);
+
+ // ContentObserver should be registered again when the user is changed
+ mSettingsObserver.registerContentObserverLocked(newUserId);
+
+ // If the system is not ready or the device is not yed unlocked by the user, then we use
+ // copy-on-write settings.
+ final boolean useCopyOnWriteSettings =
+ !mSystemReady || !mUserManagerInternal.isUserUnlockingOrUnlocked(newUserId);
+ mSettings.switchCurrentUser(newUserId, useCopyOnWriteSettings);
+ updateCurrentProfileIds();
+ // Additional subtypes should be reset when the user is changed
+ AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId);
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+
+ if (DEBUG) {
+ Slog.d(TAG, "Switching user stage 2/3. newUserId=" + newUserId
+ + " defaultImiId=" + defaultImiId);
+ }
+
+ // For secondary users, the list of enabled IMEs may not have been updated since the
+ // callbacks to PackageMonitor are ignored for the secondary user. Here, defaultImiId may
+ // not be empty even if the IME has been uninstalled by the primary user.
+ // Even in such cases, IMMS works fine because it will find the most applicable
+ // IME for that user.
+ final boolean initialUserSwitch = TextUtils.isEmpty(defaultImiId);
+ mLastSystemLocales = mRes.getConfiguration().getLocales();
+
+ // The mSystemReady flag is set during boot phase,
+ // and user switch would not happen at that time.
+ resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_USER);
+ buildInputMethodListLocked(initialUserSwitch);
+ if (TextUtils.isEmpty(mSettings.getSelectedInputMethod())) {
+ // This is the first time of the user switch and
+ // set the current ime to the proper one.
+ resetDefaultImeLocked(mContext);
+ }
+ updateFromSettingsLocked(true);
+
+ if (initialUserSwitch) {
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
+ getPackageManagerForUser(mContext, newUserId),
+ mSettings.getEnabledInputMethodListLocked());
+ }
+
+ if (DEBUG) {
+ Slog.d(TAG, "Switching user stage 3/3. newUserId=" + newUserId
+ + " selectedIme=" + mSettings.getSelectedInputMethod());
+ }
+
+ mLastSwitchUserId = newUserId;
+
+ if (mIsInteractive && clientToBeReset != null) {
+ final ClientState cs = mClients.get(clientToBeReset.asBinder());
+ if (cs == null) {
+ // The client is already gone.
+ return;
+ }
+ cs.mClient.scheduleStartInputIfNecessary(mInFullscreenMode);
+ }
+ }
+
+ void updateCurrentProfileIds() {
+ mSettings.setCurrentProfileIds(
+ mUserManagerInternal.getProfileIds(mSettings.getCurrentUserId(),
+ false /* enabledOnly */));
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ try {
+ return super.onTransact(code, data, reply, flags);
+ } catch (RuntimeException e) {
+ // The input method manager only throws security exceptions, so let's
+ // log all others.
+ if (!(e instanceof SecurityException)) {
+ Slog.wtf(TAG, "Input Method Manager Crash", e);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * TODO(b/32343335): The entire systemRunning() method needs to be revisited.
+ */
+ public void systemRunning() {
+ synchronized (ImfLock.class) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- systemReady");
+ }
+ if (!mSystemReady) {
+ mSystemReady = true;
+ mLastSystemLocales = mRes.getConfiguration().getLocales();
+ final int currentUserId = mSettings.getCurrentUserId();
+ mSettings.switchCurrentUser(currentUserId,
+ !mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId));
+ mStatusBarManagerInternal =
+ LocalServices.getService(StatusBarManagerInternal.class);
+ hideStatusBarIconLocked();
+ updateSystemUiLocked(mImeWindowVis, mBackDisposition);
+ mShowOngoingImeSwitcherForPhones = mRes.getBoolean(
+ com.android.internal.R.bool.show_ongoing_ime_switcher);
+ if (mShowOngoingImeSwitcherForPhones) {
+ mWindowManagerInternal.setOnHardKeyboardStatusChangeListener(available -> {
+ mHandler.obtainMessage(MSG_HARD_KEYBOARD_SWITCH_CHANGED,
+ available ? 1 : 0, 0 /* unused */).sendToTarget();
+ });
+ }
+
+ // begin CarInputMethodManagerService
+ mImeDrawsImeNavBarResLazyInitFuture = mExecutor.submit(() -> {
+ // Note that the synchronization block below guarantees that the task
+ // can never be completed before the returned Future<?> object is assigned to
+ // the "mImeDrawsImeNavBarResLazyInitFuture" field.
+ synchronized (ImfLock.class) {
+ mImeDrawsImeNavBarResLazyInitFuture = null;
+ if (currentUserId != mSettings.getCurrentUserId()) {
+ // This means that the current user is already switched to other user
+ // before the background task is executed. In this scenario the relevant
+ // field should already be initialized.
+ return;
+ }
+ maybeInitImeNavbarConfigLocked(currentUserId);
+ }
+ });
+ // end CarInputMethodManagerService
+
+ mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true);
+ mSettingsObserver.registerContentObserverLocked(currentUserId);
+
+ // begin CarInputMethodManagerService
+ // ImmsBroadcastReceiverForSystemUser moved to IMMS Proxy
+ // end CarInputMethodManagerService
+
+ final IntentFilter broadcastFilterForAllUsers = new IntentFilter();
+ broadcastFilterForAllUsers.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ mBroadcastReceiver = new ImmsBroadcastReceiverForAllUsers();
+ mContext.registerReceiverAsUser(mBroadcastReceiver,
+ UserHandle.ALL, broadcastFilterForAllUsers, null, null,
+ Context.RECEIVER_EXPORTED);
+
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+ final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId);
+ buildInputMethodListLocked(!imeSelectedOnBoot /* resetDefaultEnabledIme */);
+ updateFromSettingsLocked(true);
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
+ getPackageManagerForUser(mContext, currentUserId),
+ mSettings.getEnabledInputMethodListLocked());
+ }
+ }
+ }
+
+ /**
+ * Shutdown this service.
+ *
+ * This service can't be re-used once this method is invoked.
+ */
+ void systemShutdown() {
+ synchronized (ImfLock.class) {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ }
+ }
+
+ /**
+ * Returns true iff the caller is identified to be the current input method with the token.
+ * @param token The window token given to the input method when it was started.
+ * @return true if and only if non-null valid token is specified.
+ */
+ @GuardedBy("ImfLock.class")
+ private boolean calledWithValidTokenLocked(@NonNull IBinder token) {
+ if (token == null) {
+ throw new InvalidParameterException("token must not be null.");
+ }
+ if (token != getCurTokenLocked()) {
+ Slog.e(TAG, "Ignoring " + Debug.getCaller() + " due to an invalid token."
+ + " uid:" + Binder.getCallingUid() + " token:" + token);
+ return false;
+ }
+ return true;
+ }
+
+ @BinderThread
+ @Nullable
+ @Override
+ public InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ synchronized (ImfLock.class) {
+ return queryDefaultInputMethodForUserIdLocked(userId);
+ }
+ }
+
+ @BinderThread
+ @NonNull
+ @Override
+ public List<InputMethodInfo> getInputMethodList(@UserIdInt int userId,
+ @DirectBootAwareness int directBootAwareness) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ synchronized (ImfLock.class) {
+ final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
+ mSettings.getCurrentUserId(), null);
+ if (resolvedUserIds.length != 1) {
+ return Collections.emptyList();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getInputMethodListLocked(
+ resolvedUserIds[0], directBootAwareness, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @Override
+ public List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ synchronized (ImfLock.class) {
+ final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
+ mSettings.getCurrentUserId(), null);
+ if (resolvedUserIds.length != 1) {
+ return Collections.emptyList();
+ }
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getEnabledInputMethodListLocked(resolvedUserIds[0], callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @Override
+ public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+
+ synchronized (ImfLock.class) {
+ if (!isStylusHandwritingEnabled(mContext, userId)) {
+ return false;
+ }
+
+ // Check if selected IME of current user supports handwriting.
+ if (userId == mSettings.getCurrentUserId()) {
+ return mBindingController.supportsStylusHandwriting();
+ }
+
+ //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList.
+ //TODO(b/210039666): use cache.
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, true);
+ final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod());
+ return imi != null && imi.supportsStylusHandwriting();
+ }
+ }
+
+ private boolean isStylusHandwritingEnabled(
+ @NonNull Context context, @UserIdInt int userId) {
+ // If user is a profile, use preference of it`s parent profile.
+ final int profileParentUserId = mUserManagerInternal.getProfileParentId(userId);
+ if (Settings.Secure.getIntForUser(context.getContentResolver(),
+ STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE,
+ profileParentUserId) == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId,
+ @DirectBootAwareness int directBootAwareness, int callingUid) {
+ final ArrayList<InputMethodInfo> methodList;
+ final InputMethodSettings settings;
+ if (userId == mSettings.getCurrentUserId()
+ && directBootAwareness == DirectBootAwareness.AUTO) {
+ // Create a copy.
+ methodList = new ArrayList<>(mMethodList);
+ settings = mSettings;
+ } else {
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ methodList = new ArrayList<>();
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+ new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
+ methodList, directBootAwareness, mSettings.getEnabledInputMethodNames());
+ settings = new InputMethodSettings(mContext, methodMap, userId, true /* copyOnWrite */);
+ }
+ // filter caller's access to input methods
+ methodList.removeIf(imi ->
+ !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings));
+ return methodList;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private List<InputMethodInfo> getEnabledInputMethodListLocked(@UserIdInt int userId,
+ int callingUid) {
+ final ArrayList<InputMethodInfo> methodList;
+ final InputMethodSettings settings;
+ if (userId == mSettings.getCurrentUserId()) {
+ methodList = mSettings.getEnabledInputMethodListLocked();
+ settings = mSettings;
+ } else {
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ settings = new InputMethodSettings(mContext, methodMap, userId, true /* copyOnWrite */);
+ methodList = settings.getEnabledInputMethodListLocked();
+ }
+ // filter caller's access to input methods
+ methodList.removeIf(imi ->
+ !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings));
+ return methodList;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void performOnCreateInlineSuggestionsRequestLocked() {
+ mAutofillController.performOnCreateInlineSuggestionsRequest();
+ }
+
+ /**
+ * Sets current host input token.
+ *
+ * @param callerImeToken the token has been made for the current active input method
+ * @param hostInputToken the host input token of the current active input method
+ */
+ void setCurHostInputToken(@NonNull IBinder callerImeToken, @Nullable IBinder hostInputToken) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(callerImeToken)) {
+ return;
+ }
+ mCurHostInputToken = hostInputToken;
+ }
+ }
+
+ /**
+ * Gets enabled subtypes of the specified {@link InputMethodInfo}.
+ *
+ * @param imiId if null, returns enabled subtypes for the current {@link InputMethodInfo}.
+ * @param allowsImplicitlyEnabledSubtypes {@code true} to return the implicitly enabled
+ * subtypes.
+ * @param userId the user ID to be queried about.
+ */
+ @Override
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
+ boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+
+ synchronized (ImfLock.class) {
+ final int callingUid = Binder.getCallingUid();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return getEnabledInputMethodSubtypeListLocked(imiId,
+ allowsImplicitlyEnabledSubtypes, userId, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId,
+ boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) {
+ if (userId == mSettings.getCurrentUserId()) {
+ final InputMethodInfo imi;
+ String selectedMethodId = getSelectedMethodIdLocked();
+ if (imiId == null && selectedMethodId != null) {
+ imi = mMethodMap.get(selectedMethodId);
+ } else {
+ imi = mMethodMap.get(imiId);
+ }
+ if (imi == null || !canCallerAccessInputMethod(
+ imi.getPackageName(), callingUid, userId, mSettings)) {
+ return Collections.emptyList();
+ }
+ return mSettings.getEnabledInputMethodSubtypeListLocked(
+ imi, allowsImplicitlyEnabledSubtypes);
+ }
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodInfo imi = methodMap.get(imiId);
+ if (imi == null) {
+ return Collections.emptyList();
+ }
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap, userId,
+ true);
+ if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) {
+ return Collections.emptyList();
+ }
+ return settings.getEnabledInputMethodSubtypeListLocked(
+ imi, allowsImplicitlyEnabledSubtypes);
+ }
+
+ /**
+ * Called by each application process as a preparation to start interacting with
+ * {@link CarInputMethodManagerService}.
+ *
+ * <p>As a general principle, IPCs from the application process that take
+ * {@link IInputMethodClient} will be rejected without this step.</p>
+ *
+ * @param client {@link android.os.Binder} proxy that is associated with the singleton instance
+ * of {@link android.view.inputmethod.InputMethodManager} that runs on the client
+ * process
+ * @param inputConnection communication channel for the fallback {@link InputConnection}
+ * @param selfReportedDisplayId self-reported display ID to which the client is associated.
+ * Whether the client is still allowed to access to this display
+ * or not needs to be evaluated every time the client interacts
+ * with the display
+ */
+ @Override
+ public void addClient(IInputMethodClient client, IRemoteInputConnection inputConnection,
+ int selfReportedDisplayId) {
+ // Here there are two scenarios where this method is called:
+ // A. IMM is being instantiated in a different process and this is an IPC from that process
+ // B. IMM is being instantiated in the same process but Binder.clearCallingIdentity() is
+ // called in the caller side if necessary.
+ // In either case the following UID/PID should be the ones where InputMethodManager is
+ // actually running.
+ final int callerUid = Binder.getCallingUid();
+ final int callerPid = Binder.getCallingPid();
+ synchronized (ImfLock.class) {
+ // TODO: Optimize this linear search.
+ final int numClients = mClients.size();
+ for (int i = 0; i < numClients; ++i) {
+ final ClientState state = mClients.valueAt(i);
+ if (state.mUid == callerUid && state.mPid == callerPid
+ && state.mSelfReportedDisplayId == selfReportedDisplayId) {
+ throw new SecurityException("uid=" + callerUid + "/pid=" + callerPid
+ + "/displayId=" + selfReportedDisplayId + " is already registered.");
+ }
+ }
+ final ClientDeathRecipient deathRecipient = new ClientDeathRecipient(this, client);
+ try {
+ client.asBinder().linkToDeath(deathRecipient, 0 /* flags */);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(e);
+ }
+ // We cannot fully avoid race conditions where the client UID already lost the access to
+ // the given self-reported display ID, even if the client is not maliciously reporting
+ // a fake display ID. Unconditionally returning SecurityException just because the
+ // client doesn't pass display ID verification can cause many test failures hence not an
+ // option right now. At the same time
+ // context.getSystemService(InputMethodManager.class)
+ // is expected to return a valid non-null instance at any time if we do not choose to
+ // have the client crash. Thus we do not verify the display ID at all here. Instead we
+ // later check the display ID every time the client needs to interact with the specified
+ // display.
+ final IInputMethodClientInvoker clientInvoker =
+ IInputMethodClientInvoker.create(client, mHandler);
+ mClients.put(client.asBinder(), new ClientState(clientInvoker, inputConnection,
+ callerUid, callerPid, selfReportedDisplayId, deathRecipient));
+ }
+ }
+
+ void removeClient(IInputMethodClient client) {
+ synchronized (ImfLock.class) {
+ ClientState cs = mClients.remove(client.asBinder());
+ if (cs != null) {
+ client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */);
+ clearClientSessionLocked(cs);
+ clearClientSessionForAccessibilityLocked(cs);
+
+ final int numItems = mVirtualDisplayIdToParentMap.size();
+ for (int i = numItems - 1; i >= 0; --i) {
+ final VirtualDisplayInfo info = mVirtualDisplayIdToParentMap.valueAt(i);
+ if (info.mParentClient == cs) {
+ mVirtualDisplayIdToParentMap.removeAt(i);
+ }
+ }
+
+ if (mCurClient == cs) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT);
+ if (mBoundToMethod) {
+ mBoundToMethod = false;
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ // When we unbind input, we are unbinding the client, so we always
+ // unbind ime and a11y together.
+ curMethod.unbindInput();
+ AccessibilityManagerInternal.get().unbindInput();
+ }
+ }
+ mBoundToAccessibility = false;
+ mCurClient = null;
+ mCurVirtualDisplayToScreenMatrix = null;
+ }
+ if (mCurFocusedWindowClient == cs) {
+ mCurFocusedWindowClient = null;
+ mCurFocusedWindowEditorInfo = null;
+ }
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) {
+ if (mCurClient != null) {
+ if (DEBUG) {
+ Slog.v(TAG, "unbindCurrentInputLocked: client="
+ + mCurClient.mClient.asBinder());
+ }
+ if (mBoundToMethod) {
+ mBoundToMethod = false;
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.unbindInput();
+ }
+ }
+ mBoundToAccessibility = false;
+
+ // Since we set active false to current client and set mCurClient to null, let's unbind
+ // all accessibility too. That means, when input method get disconnected (including
+ // switching ime), we also unbind accessibility
+ mCurClient.mClient.setActive(false /* active */, false /* fullscreen */);
+ mCurClient.mClient.onUnbindMethod(getSequenceNumberLocked(), unbindClientReason);
+ mCurClient.mSessionRequested = false;
+ mCurClient.mSessionRequestedForAccessibility = false;
+ mCurClient = null;
+ mCurVirtualDisplayToScreenMatrix = null;
+ ImeTracker.forLogging().onFailed(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
+
+ mMenuController.hideInputMethodMenuLocked();
+ }
+ }
+
+ /** {@code true} when a {@link ClientState} has attached from starting the input connection. */
+ @GuardedBy("ImfLock.class")
+ boolean hasAttachedClient() {
+ return mCurClient != null;
+ }
+
+ @VisibleForTesting
+ void setAttachedClientForTesting(@NonNull ClientState cs) {
+ synchronized (ImfLock.class) {
+ mCurClient = cs;
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void clearInputShownLocked() {
+ mVisibilityStateComputer.setInputShown(false);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isInputShown() {
+ return mVisibilityStateComputer.isInputShown();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isShowRequestedForCurrentWindow() {
+ final ImeTargetWindowState state = mVisibilityStateComputer.getWindowStateOrNull(
+ mCurFocusedWindow);
+ return state != null && state.isRequestedImeVisible();
+ }
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
+ if (!mBoundToMethod) {
+ getCurMethodLocked().bindInput(mCurClient.mBinding);
+ mBoundToMethod = true;
+ }
+
+ final boolean restarting = !initial;
+ final Binder startInputToken = new Binder();
+ final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(),
+ getCurTokenLocked(),
+ mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting,
+ UserHandle.getUserId(mCurClient.mUid), mCurClient.mSelfReportedDisplayId,
+ mCurFocusedWindow, mCurEditorInfo, mCurFocusedWindowSoftInputMode,
+ getSequenceNumberLocked());
+ mImeTargetWindowMap.put(startInputToken, mCurFocusedWindow);
+ mStartInputHistory.addEntry(info);
+
+ // Seems that PackageManagerInternal#grantImplicitAccess() doesn't handle cross-user
+ // implicit visibility (e.g. IME[user=10] -> App[user=0]) thus we do this only for the
+ // same-user scenarios.
+ // That said ignoring cross-user scenario will never affect IMEs that do not have
+ // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case.
+ if (mSettings.getCurrentUserId() == UserHandle.getUserId(mCurClient.mUid)) {
+ mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(),
+ null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()),
+ mCurClient.mUid, true /* direct */);
+ }
+
+ @InputMethodNavButtonFlags
+ final int navButtonFlags = getInputMethodNavButtonFlagsLocked();
+ final SessionState session = mCurClient.mCurSession;
+ setEnabledSessionLocked(session);
+ session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting,
+ navButtonFlags, mCurImeDispatcher);
+ if (isShowRequestedForCurrentWindow()) {
+ if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
+ // Re-use current statsToken, if it exists.
+ final ImeTracker.Token statsToken = mCurStatsToken;
+ mCurStatsToken = null;
+ showCurrentInputLocked(mCurFocusedWindow, statsToken,
+ mVisibilityStateComputer.getImeShowFlags(),
+ null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
+ }
+
+ String curId = getCurIdLocked();
+ final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId);
+ final boolean suppressesSpellChecker =
+ curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
+ final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
+ createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
+ return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
+ session.mSession, accessibilityInputMethodSessions,
+ (session.mChannel != null ? session.mChannel.dup() : null),
+ curId, getSequenceNumberLocked(), mCurVirtualDisplayToScreenMatrix,
+ suppressesSpellChecker);
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private Matrix getVirtualDisplayToScreenMatrixLocked(int clientDisplayId, int imeDisplayId) {
+ if (clientDisplayId == imeDisplayId) {
+ return null;
+ }
+ int displayId = clientDisplayId;
+ Matrix matrix = null;
+ while (true) {
+ final VirtualDisplayInfo info = mVirtualDisplayIdToParentMap.get(displayId);
+ if (info == null) {
+ return null;
+ }
+ if (matrix == null) {
+ matrix = new Matrix(info.mMatrix);
+ } else {
+ matrix.postConcat(info.mMatrix);
+ }
+ if (info.mParentClient.mSelfReportedDisplayId == imeDisplayId) {
+ return matrix;
+ }
+ displayId = info.mParentClient.mSelfReportedDisplayId;
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void attachNewAccessibilityLocked(@StartInputReason int startInputReason,
+ boolean initial) {
+ if (!mBoundToAccessibility) {
+ AccessibilityManagerInternal.get().bindInput();
+ mBoundToAccessibility = true;
+ }
+
+ // TODO(b/187453053): grantImplicitAccess to accessibility services access? if so, need to
+ // record accessibility services uid.
+
+ // We don't start input when session for a11y is created. We start input when
+ // input method start input, a11y manager service is always on.
+ if (startInputReason != StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY) {
+ setEnabledSessionForAccessibilityLocked(mCurClient.mAccessibilitySessions);
+ AccessibilityManagerInternal.get().startInput(mCurRemoteAccessibilityInputConnection,
+ mCurEditorInfo, !initial /* restarting */);
+ }
+ }
+
+ private SparseArray<IAccessibilityInputMethodSession> createAccessibilityInputMethodSessions(
+ SparseArray<AccessibilitySessionState> accessibilitySessions) {
+ final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
+ new SparseArray<>();
+ if (accessibilitySessions != null) {
+ for (int i = 0; i < accessibilitySessions.size(); i++) {
+ accessibilityInputMethodSessions.append(accessibilitySessions.keyAt(i),
+ accessibilitySessions.valueAt(i).mSession);
+ }
+ }
+ return accessibilityInputMethodSessions;
+ }
+
+ /**
+ * Called by {@link #startInputOrWindowGainedFocusInternalLocked} to bind/unbind/attach the
+ * selected InputMethod to the given focused IME client.
+ *
+ * Note that this should be called after validating if the IME client has IME focus.
+ *
+ * @see WindowManagerInternal#hasInputMethodClientFocus(IBinder, int, int, int)
+ */
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private InputBindResult startInputUncheckedLocked(@NonNull ClientState cs,
+ IRemoteInputConnection inputConnection,
+ @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
+ @NonNull EditorInfo editorInfo, @StartInputFlags int startInputFlags,
+ @StartInputReason int startInputReason,
+ int unverifiedTargetSdkVersion,
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ // If no method is currently selected, do nothing.
+ final String selectedMethodId = getSelectedMethodIdLocked();
+ if (selectedMethodId == null) {
+ return InputBindResult.NO_IME;
+ }
+
+ if (!mSystemReady) {
+ // If the system is not yet ready, we shouldn't be running third
+ // party code.
+ return new InputBindResult(
+ InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
+ null, null, null, selectedMethodId, getSequenceNumberLocked(), null, false);
+ }
+
+ if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid,
+ editorInfo.packageName)) {
+ Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ + " uid=" + cs.mUid + " package=" + editorInfo.packageName);
+ return InputBindResult.INVALID_PACKAGE_NAME;
+ }
+
+ // Compute the final shown display ID with validated cs.selfReportedDisplayId for this
+ // session & other conditions.
+ ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull(
+ mCurFocusedWindow);
+ if (winState == null) {
+ return InputBindResult.NOT_IME_TARGET_WINDOW;
+ }
+ final int csDisplayId = cs.mSelfReportedDisplayId;
+ mDisplayIdToShowIme = mVisibilityStateComputer.computeImeDisplayId(winState, csDisplayId);
+
+ if (mVisibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
+ return InputBindResult.NO_IME;
+ }
+
+ if (mCurClient != cs) {
+ prepareClientSwitchLocked(cs);
+ }
+
+ // Bump up the sequence for this client and attach it.
+ advanceSequenceNumberLocked();
+ mCurClient = cs;
+ mCurInputConnection = inputConnection;
+ mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection;
+ mCurImeDispatcher = imeDispatcher;
+ mCurVirtualDisplayToScreenMatrix =
+ getVirtualDisplayToScreenMatrixLocked(cs.mSelfReportedDisplayId,
+ mDisplayIdToShowIme);
+ // Override the locale hints if the app is running on a virtual device.
+ if (mVdmInternal == null) {
+ mVdmInternal = LocalServices.getService(VirtualDeviceManagerInternal.class);
+ }
+ if (mVdmInternal != null && editorInfo.hintLocales == null) {
+ LocaleList hintsFromVirtualDevice = mVdmInternal.getPreferredLocaleListForUid(cs.mUid);
+ if (hintsFromVirtualDevice != null) {
+ editorInfo.hintLocales = hintsFromVirtualDevice;
+ }
+ }
+ mCurEditorInfo = editorInfo;
+
+ // If configured, we want to avoid starting up the IME if it is not supposed to be showing
+ if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
+ unverifiedTargetSdkVersion)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
+ }
+ invalidateAutofillSessionLocked();
+ mBindingController.unbindCurrentMethod();
+ return InputBindResult.NO_EDITOR;
+ }
+
+ // Check if the input method is changing.
+ // We expect the caller has already verified that the client is allowed to access this
+ // display ID.
+ if (isSelectedMethodBoundLocked()) {
+ if (cs.mCurSession != null) {
+ // Fast case: if we are already connected to the input method,
+ // then just return it.
+ // This doesn't mean a11y sessions are there. When a11y service is
+ // enabled while this client is switched out, this client doesn't have the session.
+ // A11yManagerService will only request missing sessions (will not request existing
+ // sessions again). Note when an a11y service is disabled, it will clear its
+ // session from all clients, so we don't need to worry about disabled a11y services.
+ cs.mSessionRequestedForAccessibility = false;
+ requestClientSessionForAccessibilityLocked(cs);
+ // we can always attach to accessibility because AccessibilityManagerService is
+ // always on.
+ attachNewAccessibilityLocked(startInputReason,
+ (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
+ return attachNewInputLocked(startInputReason,
+ (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
+ }
+
+ InputBindResult bindResult = tryReuseConnectionLocked(cs);
+ if (bindResult != null) {
+ return bindResult;
+ }
+ }
+
+ mBindingController.unbindCurrentMethod();
+
+ return mBindingController.bindCurrentMethod();
+ }
+
+ @GuardedBy("ImfLock.class")
+ void invalidateAutofillSessionLocked() {
+ mAutofillController.invalidateAutofillSession();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean shouldPreventImeStartupLocked(
+ @NonNull String selectedMethodId,
+ @StartInputFlags int startInputFlags,
+ int unverifiedTargetSdkVersion) {
+ // Fast-path for the majority of cases
+ if (!mPreventImeStartupUnlessTextEditor) {
+ return false;
+ }
+ if (isShowRequestedForCurrentWindow()) {
+ return false;
+ }
+ if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) {
+ return false;
+ }
+ final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+ if (imi == null) {
+ return false;
+ }
+ if (ArrayUtils.contains(mNonPreemptibleInputMethods, imi.getPackageName())) {
+ return false;
+ }
+ return true;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean isSelectedMethodBoundLocked() {
+ String curId = getCurIdLocked();
+ return curId != null && curId.equals(getSelectedMethodIdLocked())
+ && mDisplayIdToShowIme == mCurTokenDisplayId;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void prepareClientSwitchLocked(ClientState cs) {
+ // If the client is changing, we need to switch over to the new
+ // one.
+ unbindCurrentClientLocked(UnbindReason.SWITCH_CLIENT);
+ // If the screen is on, inform the new client it is active
+ if (mIsInteractive) {
+ cs.mClient.setActive(true /* active */, false /* fullscreen */);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private InputBindResult tryReuseConnectionLocked(@NonNull ClientState cs) {
+ if (hasConnectionLocked()) {
+ if (getCurMethodLocked() != null) {
+ // Return to client, and we will get back with it when
+ // we have had a session made for it.
+ requestClientSessionLocked(cs);
+ requestClientSessionForAccessibilityLocked(cs);
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
+ null, null, null, getCurIdLocked(), getSequenceNumberLocked(), null, false);
+ } else {
+ long bindingDuration = SystemClock.uptimeMillis() - getLastBindTimeLocked();
+ if (bindingDuration < TIME_TO_RECONNECT) {
+ // In this case we have connected to the service, but
+ // don't yet have its interface. If it hasn't been too
+ // long since we did the connection, we'll return to
+ // the client and wait to get the service interface so
+ // we can report back. If it has been too long, we want
+ // to fall through so we can try a disconnect/reconnect
+ // to see if we can get back in touch with the service.
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
+ null, null, null, getCurIdLocked(), getSequenceNumberLocked(), null,
+ false);
+ } else {
+ EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
+ getSelectedMethodIdLocked(), bindingDuration, 0);
+ }
+ }
+ }
+ return null;
+ }
+
+ @FunctionalInterface
+ interface ImeDisplayValidator {
+ @DisplayImePolicy int getDisplayImePolicy(int displayId);
+ }
+
+ /**
+ * Find the display where the IME should be shown.
+ *
+ * @param displayId the ID of the display where the IME client target is.
+ * @param checker instance of {@link ImeDisplayValidator} which is used for
+ * checking display config to adjust the final target display.
+ * @return The ID of the display where the IME should be shown or
+ * {@link android.view.Display#INVALID_DISPLAY} if the display has an ImePolicy of
+ * {@link WindowManager#DISPLAY_IME_POLICY_HIDE}.
+ */
+ static int computeImeDisplayIdForTarget(int displayId, @NonNull ImeDisplayValidator checker) {
+ if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) {
+ return FALLBACK_DISPLAY_ID;
+ }
+
+ // Show IME window on fallback display when the display doesn't support system decorations
+ // or the display is virtual and isn't owned by system for security concern.
+ final int result = checker.getDisplayImePolicy(displayId);
+ if (result == DISPLAY_IME_POLICY_LOCAL) {
+ return displayId;
+ } else if (result == DISPLAY_IME_POLICY_HIDE) {
+ return INVALID_DISPLAY;
+ } else {
+ return FALLBACK_DISPLAY_ID;
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void initializeImeLocked(@NonNull IInputMethodInvoker inputMethod, @NonNull IBinder token) {
+ if (DEBUG) {
+ Slog.v(TAG, "Sending attach of token: " + token + " for display: "
+ + mCurTokenDisplayId);
+ }
+ inputMethod.initializeInternal(token, new InputMethodPrivilegedOperationsImpl(this, token),
+ getInputMethodNavButtonFlagsLocked());
+ }
+
+ @AnyThread
+ void scheduleResetStylusHandwriting() {
+ mHandler.obtainMessage(MSG_RESET_HANDWRITING).sendToTarget();
+ }
+
+ @AnyThread
+ void schedulePrepareStylusHandwritingDelegation(
+ @NonNull String delegatePackageName, @NonNull String delegatorPackageName) {
+ mHandler.obtainMessage(
+ MSG_PREPARE_HANDWRITING_DELEGATION,
+ new Pair<>(delegatePackageName, delegatorPackageName)).sendToTarget();
+ }
+
+ @AnyThread
+ void scheduleRemoveStylusHandwritingWindow() {
+ mHandler.obtainMessage(MSG_REMOVE_HANDWRITING_WINDOW).sendToTarget();
+ }
+
+ @AnyThread
+ void scheduleNotifyImeUidToAudioService(int uid) {
+ mHandler.removeMessages(MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE);
+ mHandler.obtainMessage(MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE, uid, 0 /* unused */)
+ .sendToTarget();
+ }
+
+ @BinderThread
+ void onSessionCreated(IInputMethodInvoker method, IInputMethodSession session,
+ InputChannel channel) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.onSessionCreated");
+ try {
+ synchronized (ImfLock.class) {
+ if (mUserSwitchHandlerTask != null) {
+ // We have a pending user-switching task so it's better to just ignore this
+ // session.
+ channel.dispose();
+ return;
+ }
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null && method != null
+ && curMethod.asBinder() == method.asBinder()) {
+ if (mCurClient != null) {
+ clearClientSessionLocked(mCurClient);
+ mCurClient.mCurSession = new SessionState(mCurClient,
+ method, session, channel);
+ InputBindResult res = attachNewInputLocked(
+ StartInputReason.SESSION_CREATED_BY_IME, true);
+ attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true);
+ if (res.method != null) {
+ mCurClient.mClient.onBindMethod(res);
+ }
+ return;
+ }
+ }
+ }
+
+ // Session abandoned. Close its associated input channel.
+ channel.dispose();
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void resetSystemUiLocked() {
+ // Set IME window status as invisible when unbinding current method.
+ mImeWindowVis = 0;
+ mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT;
+ updateSystemUiLocked(mImeWindowVis, mBackDisposition);
+ mCurTokenDisplayId = INVALID_DISPLAY;
+ mCurHostInputToken = null;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason) {
+ setSelectedMethodIdLocked(null);
+ mBindingController.unbindCurrentMethod();
+ unbindCurrentClientLocked(unbindClientReason);
+ }
+
+ @GuardedBy("ImfLock.class")
+ void reRequestCurrentClientSessionLocked() {
+ if (mCurClient != null) {
+ clearClientSessionLocked(mCurClient);
+ clearClientSessionForAccessibilityLocked(mCurClient);
+ requestClientSessionLocked(mCurClient);
+ requestClientSessionForAccessibilityLocked(mCurClient);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void requestClientSessionLocked(ClientState cs) {
+ if (!cs.mSessionRequested) {
+ if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
+ final InputChannel serverChannel;
+ final InputChannel clientChannel;
+ {
+ final InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
+ serverChannel = channels[0];
+ clientChannel = channels[1];
+ }
+
+ cs.mSessionRequested = true;
+
+ final IInputMethodInvoker curMethod = getCurMethodLocked();
+ final IInputMethodSessionCallback.Stub callback =
+ new IInputMethodSessionCallback.Stub() {
+ @Override
+ public void sessionCreated(IInputMethodSession session) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ onSessionCreated(curMethod, session, serverChannel);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ };
+
+ try {
+ curMethod.createSession(clientChannel, callback);
+ } finally {
+ // Dispose the channel because the remote proxy will get its own copy when
+ // unparceled.
+ if (clientChannel != null) {
+ clientChannel.dispose();
+ }
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void requestClientSessionForAccessibilityLocked(ClientState cs) {
+ if (!cs.mSessionRequestedForAccessibility) {
+ if (DEBUG) Slog.v(TAG, "Creating new accessibility sessions for client " + cs);
+ cs.mSessionRequestedForAccessibility = true;
+ ArraySet<Integer> ignoreSet = new ArraySet<>();
+ for (int i = 0; i < cs.mAccessibilitySessions.size(); i++) {
+ ignoreSet.add(cs.mAccessibilitySessions.keyAt(i));
+ }
+ AccessibilityManagerInternal.get().createImeSession(ignoreSet);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void clearClientSessionLocked(ClientState cs) {
+ finishSessionLocked(cs.mCurSession);
+ cs.mCurSession = null;
+ cs.mSessionRequested = false;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void clearClientSessionForAccessibilityLocked(ClientState cs) {
+ for (int i = 0; i < cs.mAccessibilitySessions.size(); i++) {
+ finishSessionForAccessibilityLocked(cs.mAccessibilitySessions.valueAt(i));
+ }
+ cs.mAccessibilitySessions.clear();
+ cs.mSessionRequestedForAccessibility = false;
+ }
+
+ @GuardedBy("ImfLock.class")
+ void clearClientSessionForAccessibilityLocked(ClientState cs, int id) {
+ AccessibilitySessionState session = cs.mAccessibilitySessions.get(id);
+ if (session != null) {
+ finishSessionForAccessibilityLocked(session);
+ cs.mAccessibilitySessions.remove(id);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void finishSessionLocked(SessionState sessionState) {
+ if (sessionState != null) {
+ if (sessionState.mSession != null) {
+ try {
+ sessionState.mSession.finishSession();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Session failed to close due to remote exception", e);
+ updateSystemUiLocked(0 /* vis */, mBackDisposition);
+ }
+ sessionState.mSession = null;
+ }
+ if (sessionState.mChannel != null) {
+ sessionState.mChannel.dispose();
+ sessionState.mChannel = null;
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void finishSessionForAccessibilityLocked(AccessibilitySessionState sessionState) {
+ if (sessionState != null) {
+ if (sessionState.mSession != null) {
+ try {
+ sessionState.mSession.finishSession();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Session failed to close due to remote exception", e);
+ }
+ sessionState.mSession = null;
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void clearClientSessionsLocked() {
+ if (getCurMethodLocked() != null) {
+ final int numClients = mClients.size();
+ for (int i = 0; i < numClients; ++i) {
+ clearClientSessionLocked(mClients.valueAt(i));
+ clearClientSessionForAccessibilityLocked(mClients.valueAt(i));
+ }
+
+ finishSessionLocked(mEnabledSession);
+ for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
+ finishSessionForAccessibilityLocked(mEnabledAccessibilitySessions.valueAt(i));
+ }
+ mEnabledSession = null;
+ mEnabledAccessibilitySessions.clear();
+ scheduleNotifyImeUidToAudioService(Process.INVALID_UID);
+ }
+ hideStatusBarIconLocked();
+ mInFullscreenMode = false;
+ mWindowManagerInternal.setDismissImeOnBackKeyPressed(false);
+ }
+
+ @BinderThread
+ private void updateStatusIcon(@NonNull IBinder token, String packageName,
+ @DrawableRes int iconId) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (iconId == 0) {
+ if (DEBUG) Slog.d(TAG, "hide the small icon for the input method");
+ hideStatusBarIconLocked();
+ } else if (packageName != null) {
+ if (DEBUG) Slog.d(TAG, "show a small icon for the input method");
+ final PackageManager userAwarePackageManager =
+ getPackageManagerForUser(mContext, mSettings.getCurrentUserId());
+ ApplicationInfo applicationInfo = null;
+ try {
+ applicationInfo = userAwarePackageManager.getApplicationInfo(packageName,
+ PackageManager.ApplicationInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ final CharSequence contentDescription = applicationInfo != null
+ ? userAwarePackageManager.getApplicationLabel(applicationInfo)
+ : null;
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setIcon(mSlotIme, packageName, iconId, 0,
+ contentDescription != null
+ ? contentDescription.toString() : null);
+ mStatusBarManagerInternal.setIconVisibility(mSlotIme, true);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void hideStatusBarIconLocked() {
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setIconVisibility(mSlotIme, false);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @InputMethodNavButtonFlags
+ private int getInputMethodNavButtonFlagsLocked() {
+ if (mImeDrawsImeNavBarResLazyInitFuture != null) {
+ // TODO(b/225366708): Avoid Future.get(), which is internally used here.
+ ConcurrentUtils.waitForFutureNoInterrupt(mImeDrawsImeNavBarResLazyInitFuture,
+ "Waiting for the lazy init of mImeDrawsImeNavBarRes");
+ }
+ final boolean canImeDrawsImeNavBar =
+ mImeDrawsImeNavBarRes != null && mImeDrawsImeNavBarRes.get();
+ final boolean shouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherLocked(
+ InputMethodService.IME_ACTIVE | InputMethodService.IME_VISIBLE);
+ return (canImeDrawsImeNavBar ? InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR : 0)
+ | (shouldShowImeSwitcherWhenImeIsShown
+ ? InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean shouldShowImeSwitcherLocked(int visibility) {
+ if (!mShowOngoingImeSwitcherForPhones) return false;
+ if (mMenuController.getSwitchingDialogLocked() != null) return false;
+ if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) {
+ return false;
+ }
+ if ((visibility & InputMethodService.IME_ACTIVE) == 0
+ || (visibility & InputMethodService.IME_INVISIBLE) != 0) {
+ return false;
+ }
+ if (mWindowManagerInternal.isHardKeyboardAvailable()) {
+ // When physical keyboard is attached, we show the ime switcher (or notification if
+ // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently
+ // exists in the IME switcher dialog. Might be OK to remove this condition once
+ // SHOW_IME_WITH_HARD_KEYBOARD settings finds a good place to live.
+ return true;
+ } else if ((visibility & InputMethodService.IME_VISIBLE) == 0) {
+ return false;
+ }
+
+ List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked(
+ InputMethodInfo::shouldShowInInputMethodPicker);
+ final int numImes = imes.size();
+ if (numImes > 2) return true;
+ if (numImes < 1) return false;
+ int nonAuxCount = 0;
+ int auxCount = 0;
+ InputMethodSubtype nonAuxSubtype = null;
+ InputMethodSubtype auxSubtype = null;
+ for (int i = 0; i < numImes; ++i) {
+ final InputMethodInfo imi = imes.get(i);
+ final List<InputMethodSubtype> subtypes =
+ mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ final int subtypeCount = subtypes.size();
+ if (subtypeCount == 0) {
+ ++nonAuxCount;
+ } else {
+ for (int j = 0; j < subtypeCount; ++j) {
+ final InputMethodSubtype subtype = subtypes.get(j);
+ if (!subtype.isAuxiliary()) {
+ ++nonAuxCount;
+ nonAuxSubtype = subtype;
+ } else {
+ ++auxCount;
+ auxSubtype = subtype;
+ }
+ }
+ }
+ }
+ if (nonAuxCount > 1 || auxCount > 1) {
+ return true;
+ } else if (nonAuxCount == 1 && auxCount == 1) {
+ if (nonAuxSubtype != null && auxSubtype != null
+ && (nonAuxSubtype.getLocale().equals(auxSubtype.getLocale())
+ || auxSubtype.overridesImplicitlyEnabledSubtype()
+ || nonAuxSubtype.overridesImplicitlyEnabledSubtype())
+ && nonAuxSubtype.containsExtraValueKey(TAG_TRY_SUPPRESSING_IME_SWITCHER)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @BinderThread
+ @SuppressWarnings("deprecation")
+ private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition) {
+ final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId();
+
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ // Skip update IME status when current token display is not same as focused display.
+ // Note that we still need to update IME status when focusing external display
+ // that does not support system decoration and fallback to show IME on default
+ // display since it is intentional behavior.
+ if (mCurTokenDisplayId != topFocusedDisplayId
+ && mCurTokenDisplayId != FALLBACK_DISPLAY_ID) {
+ return;
+ }
+ mImeWindowVis = vis;
+ mBackDisposition = backDisposition;
+ updateSystemUiLocked(vis, backDisposition);
+ }
+
+ final boolean dismissImeOnBackKeyPressed;
+ switch (backDisposition) {
+ case InputMethodService.BACK_DISPOSITION_WILL_DISMISS:
+ dismissImeOnBackKeyPressed = true;
+ break;
+ case InputMethodService.BACK_DISPOSITION_WILL_NOT_DISMISS:
+ dismissImeOnBackKeyPressed = false;
+ break;
+ default:
+ case InputMethodService.BACK_DISPOSITION_DEFAULT:
+ dismissImeOnBackKeyPressed = ((vis & InputMethodService.IME_VISIBLE) != 0);
+ break;
+ }
+ mWindowManagerInternal.setDismissImeOnBackKeyPressed(dismissImeOnBackKeyPressed);
+ }
+
+ @BinderThread
+ private void reportStartInput(@NonNull IBinder token, IBinder startInputToken) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final IBinder targetWindow = mImeTargetWindowMap.get(startInputToken);
+ if (targetWindow != null) {
+ mWindowManagerInternal.updateInputMethodTargetWindow(token, targetWindow);
+ }
+ mLastImeTargetWindow = targetWindow;
+ }
+ }
+
+ private void updateImeWindowStatus(boolean disableImeIcon) {
+ synchronized (ImfLock.class) {
+ if (disableImeIcon) {
+ updateSystemUiLocked(0, mBackDisposition);
+ } else {
+ updateSystemUiLocked();
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void updateSystemUiLocked() {
+ updateSystemUiLocked(mImeWindowVis, mBackDisposition);
+ }
+
+ // Caution! This method is called in this class. Handle multi-user carefully
+ @GuardedBy("ImfLock.class")
+ private void updateSystemUiLocked(int vis, int backDisposition) {
+ if (getCurTokenLocked() == null) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "IME window vis: " + vis
+ + " active: " + (vis & InputMethodService.IME_ACTIVE)
+ + " inv: " + (vis & InputMethodService.IME_INVISIBLE)
+ + " displayId: " + mCurTokenDisplayId);
+ }
+
+ // TODO: Move this clearing calling identity block to setImeWindowStatus after making sure
+ // all updateSystemUi happens on system privilege.
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (!mCurPerceptible) {
+ if ((vis & InputMethodService.IME_VISIBLE) != 0) {
+ vis &= ~InputMethodService.IME_VISIBLE;
+ vis |= InputMethodService.IME_VISIBLE_IMPERCEPTIBLE;
+ }
+ } else {
+ vis &= ~InputMethodService.IME_VISIBLE_IMPERCEPTIBLE;
+ }
+ // mImeWindowVis should be updated before calling shouldShowImeSwitcherLocked().
+ final boolean needsToShowImeSwitcher = shouldShowImeSwitcherLocked(vis);
+ if (mStatusBarManagerInternal != null) {
+ mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
+ getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void updateFromSettingsLocked(boolean enabledMayChange) {
+ updateInputMethodsFromSettingsLocked(enabledMayChange);
+ mMenuController.updateKeyboardFromSettingsLocked();
+ }
+
+ @GuardedBy("ImfLock.class")
+ void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) {
+ if (enabledMayChange) {
+ final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext,
+ mSettings.getCurrentUserId());
+
+ List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ for (int i = 0; i < enabled.size(); i++) {
+ // We allow the user to select "disabled until used" apps, so if they
+ // are enabling one of those here we now need to make it enabled.
+ InputMethodInfo imm = enabled.get(i);
+ ApplicationInfo ai = null;
+ try {
+ ai = userAwarePackageManager.getApplicationInfo(imm.getPackageName(),
+ PackageManager.ApplicationInfoFlags.of(
+ PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS));
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ if (ai != null && ai.enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Update state(" + imm.getId()
+ + "): DISABLED_UNTIL_USED -> DEFAULT");
+ }
+ userAwarePackageManager.setApplicationEnabledSetting(imm.getPackageName(),
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
+ PackageManager.DONT_KILL_APP);
+ }
+ }
+ }
+ // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and
+ // ENABLED_INPUT_METHODS is taking care of keeping them correctly in
+ // sync, so we will never have a DEFAULT_INPUT_METHOD that is not
+ // enabled.
+ String id = mSettings.getSelectedInputMethod();
+ // There is no input method selected, try to choose new applicable input method.
+ if (TextUtils.isEmpty(id) && chooseNewDefaultIMELocked()) {
+ id = mSettings.getSelectedInputMethod();
+ }
+ if (!TextUtils.isEmpty(id)) {
+ try {
+ setInputMethodLocked(id, mSettings.getSelectedInputMethodSubtypeId(id));
+ } catch (IllegalArgumentException e) {
+ Slog.w(TAG, "Unknown input method from prefs: " + id, e);
+ resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED);
+ }
+ } else {
+ // There is no longer an input method set, so stop any current one.
+ resetCurrentMethodAndClientLocked(UnbindReason.NO_IME);
+ }
+ // Here is not the perfect place to reset the switching controller. Ideally
+ // mSwitchingController and mSettings should be able to share the same state.
+ // TODO: Make sure that mSwitchingController and mSettings are sharing the
+ // the same enabled IMEs list.
+ mSwitchingController.resetCircularListLocked(mContext);
+ mHardwareKeyboardShortcutController.reset(mSettings);
+
+ sendOnNavButtonFlagsChangedLocked();
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void notifyInputMethodSubtypeChangedLocked(@UserIdInt int userId,
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+ final InputMethodSubtype normalizedSubtype =
+ subtype != null && subtype.isSuitableForPhysicalKeyboardLayoutMapping()
+ ? subtype : null;
+ final InputMethodSubtypeHandle newSubtypeHandle = normalizedSubtype != null
+ ? InputMethodSubtypeHandle.of(imi, normalizedSubtype) : null;
+ mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping(
+ userId, newSubtypeHandle, normalizedSubtype);
+ }
+
+ @GuardedBy("ImfLock.class")
+ void setInputMethodLocked(String id, int subtypeId) {
+ InputMethodInfo info = mMethodMap.get(id);
+ if (info == null) {
+ throw getExceptionForUnknownImeId(id);
+ }
+
+ // See if we need to notify a subtype change within the same IME.
+ if (id.equals(getSelectedMethodIdLocked())) {
+ final int userId = mSettings.getCurrentUserId();
+ final int subtypeCount = info.getSubtypeCount();
+ if (subtypeCount <= 0) {
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
+ return;
+ }
+ final InputMethodSubtype oldSubtype = mCurrentSubtype;
+ final InputMethodSubtype newSubtype;
+ if (subtypeId >= 0 && subtypeId < subtypeCount) {
+ newSubtype = info.getSubtypeAt(subtypeId);
+ } else {
+ // If subtype is null, try to find the most applicable one from
+ // getCurrentInputMethodSubtype.
+ newSubtype = getCurrentInputMethodSubtypeLocked();
+ }
+ if (newSubtype == null || oldSubtype == null) {
+ Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype
+ + ", new subtype = " + newSubtype);
+ notifyInputMethodSubtypeChangedLocked(userId, info, null);
+ return;
+ }
+ if (!newSubtype.equals(oldSubtype)) {
+ setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true);
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ updateSystemUiLocked(mImeWindowVis, mBackDisposition);
+ curMethod.changeInputMethodSubtype(newSubtype);
+ }
+ }
+ return;
+ }
+
+ // Changing to a different IME.
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ // Set a subtype to this input method.
+ // subtypeId the name of a subtype which will be set.
+ setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false);
+ // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked()
+ // because mCurMethodId is stored as a history in
+ // setSelectedInputMethodAndSubtypeLocked().
+ setSelectedMethodIdLocked(id);
+
+ if (mActivityManagerInternal.isSystemReady()) {
+ Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ intent.putExtra("input_method_id", id);
+ mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+ }
+ unbindCurrentClientLocked(UnbindReason.SWITCH_IME);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
+ int uid = Binder.getCallingUid();
+ ImeTracing.getInstance().triggerManagerServiceDump(
+ "InputMethodManagerService#showSoftInput");
+ synchronized (ImfLock.class) {
+ if (!canInteractWithImeLocked(uid, client, "showSoftInput", statsToken)) {
+ ImeTracker.forLogging().onFailed(
+ statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ return false;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
+ return showCurrentInputLocked(windowToken, statsToken, flags, lastClickTooType,
+ resultReceiver, reason);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void startStylusHandwriting(IInputMethodClient client) {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.startStylusHandwriting");
+ try {
+ ImeTracing.getInstance().triggerManagerServiceDump(
+ "InputMethodManagerService#startStylusHandwriting");
+ int uid = Binder.getCallingUid();
+ synchronized (ImfLock.class) {
+ mHwController.clearPendingHandwritingDelegation();
+ if (!canInteractWithImeLocked(uid, client, "startStylusHandwriting",
+ null /* statsToken */)) {
+ return;
+ }
+ if (!hasSupportedStylusLocked()) {
+ Slog.w(TAG, "No supported Stylus hardware found on device. Ignoring"
+ + " startStylusHandwriting()");
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (!mBindingController.supportsStylusHandwriting()) {
+ Slog.w(TAG,
+ "Stylus HW unsupported by IME. Ignoring startStylusHandwriting()");
+ return;
+ }
+
+ final OptionalInt requestId = mHwController.getCurrentRequestId();
+ if (!requestId.isPresent()) {
+ Slog.e(TAG, "Stylus handwriting was not initialized.");
+ return;
+ }
+ if (!mHwController.isStylusGestureOngoing()) {
+ Slog.e(TAG,
+ "There is no ongoing stylus gesture to start stylus handwriting.");
+ return;
+ }
+ if (mHwController.hasOngoingStylusHandwritingSession()) {
+ // prevent duplicate calls to startStylusHandwriting().
+ Slog.e(TAG,
+ "Stylus handwriting session is already ongoing."
+ + " Ignoring startStylusHandwriting().");
+ return;
+ }
+ if (DEBUG) Slog.v(TAG, "Client requesting Stylus Handwriting to be started");
+ final IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.canStartStylusHandwriting(requestId.getAsInt());
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @Override
+ public void prepareStylusHandwritingDelegation(
+ @NonNull IInputMethodClient client,
+ @UserIdInt int userId,
+ @NonNull String delegatePackageName,
+ @NonNull String delegatorPackageName) {
+ if (!isStylusHandwritingEnabled(mContext, userId)) {
+ Slog.w(TAG, "Can not prepare stylus handwriting delegation. Stylus handwriting"
+ + " pref is disabled for user: " + userId);
+ return;
+ }
+ if (!verifyClientAndPackageMatch(client, delegatorPackageName)) {
+ Slog.w(TAG, "prepareStylusHandwritingDelegation() fail");
+ throw new IllegalArgumentException("Delegator doesn't match Uid");
+ }
+ schedulePrepareStylusHandwritingDelegation(delegatePackageName, delegatorPackageName);
+ }
+
+ @Override
+ public boolean acceptStylusHandwritingDelegation(
+ @NonNull IInputMethodClient client,
+ @UserIdInt int userId,
+ @NonNull String delegatePackageName,
+ @NonNull String delegatorPackageName) {
+ if (!isStylusHandwritingEnabled(mContext, userId)) {
+ Slog.w(TAG, "Can not accept stylus handwriting delegation. Stylus handwriting"
+ + " pref is disabled for user: " + userId);
+ return false;
+ }
+ if (!verifyDelegator(client, delegatePackageName, delegatorPackageName)) {
+ return false;
+ }
+
+ startStylusHandwriting(client);
+ return true;
+ }
+
+ private boolean verifyClientAndPackageMatch(
+ @NonNull IInputMethodClient client, @NonNull String packageName) {
+ ClientState cs;
+ synchronized (ImfLock.class) {
+ cs = mClients.get(client.asBinder());
+ }
+ if (cs == null) {
+ throw new IllegalArgumentException("unknown client " + client.asBinder());
+ }
+ return InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal, cs.mUid, packageName);
+ }
+
+ private boolean verifyDelegator(
+ @NonNull IInputMethodClient client,
+ @NonNull String delegatePackageName,
+ @NonNull String delegatorPackageName) {
+ if (!verifyClientAndPackageMatch(client, delegatePackageName)) {
+ Slog.w(TAG, "Delegate package does not belong to the same user. Ignoring"
+ + " startStylusHandwriting");
+ return false;
+ }
+ synchronized (ImfLock.class) {
+ if (!delegatorPackageName.equals(mHwController.getDelegatorPackageName())) {
+ Slog.w(TAG,
+ "Delegator package does not match. Ignoring startStylusHandwriting");
+ return false;
+ }
+ if (!delegatePackageName.equals(mHwController.getDelegatePackageName())) {
+ Slog.w(TAG,
+ "Delegate package does not match. Ignoring startStylusHandwriting");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @BinderThread
+ @Override
+ public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) {
+ Binder.withCleanCallingIdentity(() -> {
+ Objects.requireNonNull(windowToken, "windowToken must not be null");
+ synchronized (ImfLock.class) {
+ if (mCurFocusedWindow != windowToken || mCurPerceptible == perceptible) {
+ return;
+ }
+ mCurPerceptible = perceptible;
+ updateSystemUiLocked();
+ }
+ });
+ }
+
+ @GuardedBy("ImfLock.class")
+ boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ return showCurrentInputLocked(windowToken, statsToken, flags,
+ MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean showCurrentInputLocked(IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ // Create statsToken is none exists.
+ if (statsToken == null) {
+ statsToken = createStatsTokenForFocusedClient(true /* show */,
+ ImeTracker.ORIGIN_SERVER_START_INPUT, reason);
+ }
+
+ if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) {
+ return false;
+ }
+
+ if (!mSystemReady) {
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
+ return false;
+ }
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY);
+
+ mVisibilityStateComputer.requestImeVisibility(windowToken, true);
+
+ // Ensure binding the connection when IME is going to show.
+ mBindingController.setCurrentMethodVisible();
+ final IInputMethodInvoker curMethod = getCurMethodLocked();
+ ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ if (curMethod != null) {
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
+ mCurStatsToken = null;
+
+ if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+ curMethod.updateEditorToolType(lastClickToolType);
+ }
+ mVisibilityApplier.performShowIme(windowToken, statsToken,
+ mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason);
+ mVisibilityStateComputer.setInputShown(true);
+ return true;
+ } else {
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = statsToken;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
+ int uid = Binder.getCallingUid();
+ ImeTracing.getInstance().triggerManagerServiceDump(
+ "InputMethodManagerService#hideSoftInput");
+ synchronized (ImfLock.class) {
+ if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) {
+ if (isInputShown()) {
+ ImeTracker.forLogging().onFailed(
+ statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ } else {
+ ImeTracker.forLogging().onCancelled(statsToken,
+ ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ }
+ return false;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInput");
+ if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
+ return CarInputMethodManagerService.this.hideCurrentInputLocked(windowToken,
+ statsToken, flags, resultReceiver, reason);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
+ int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ // Create statsToken is none exists.
+ if (statsToken == null) {
+ statsToken = createStatsTokenForFocusedClient(false /* show */,
+ ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+ }
+
+ if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) {
+ return false;
+ }
+
+ // There is a chance that IMM#hideSoftInput() is called in a transient state where
+ // IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting
+ // to be updated with the new value sent from IME process. Even in such a transient state
+ // historically we have accepted an incoming call of IMM#hideSoftInput() from the
+ // application process as a valid request, and have even promised such a behavior with CTS
+ // since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only
+ // IMMS#InputShown indicates that the software keyboard is shown.
+ // TODO(b/246309664): Clean up IMMS#mImeWindowVis
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ final boolean shouldHideSoftInput = curMethod != null
+ && (isInputShown() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
+
+ mVisibilityStateComputer.requestImeVisibility(windowToken, false);
+ if (shouldHideSoftInput) {
+ // The IME will report its visible state again after the following message finally
+ // delivered to the IME process as an IPC. Hence the inconsistency between
+ // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
+ // the final state.
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
+ mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason);
+ } else {
+ ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
+ }
+ mBindingController.setCurrentMethodNotVisible();
+ mVisibilityStateComputer.clearImeShowFlags();
+ // Cancel existing statsToken for show IME as we got a hide request.
+ ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
+ mCurStatsToken = null;
+ return shouldHideSoftInput;
+ }
+
+ private boolean isImeClientFocused(IBinder windowToken, ClientState cs) {
+ final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
+ windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId);
+ return imeClientFocus == WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS;
+ }
+
+ @NonNull
+ @Override
+ public InputBindResult startInputOrWindowGainedFocus(
+ @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken,
+ @StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
+ int windowFlags, @Nullable EditorInfo editorInfo,
+ IRemoteInputConnection inputConnection,
+ IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
+ int unverifiedTargetSdkVersion, @UserIdInt int userId,
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+
+ if (editorInfo == null || editorInfo.targetInputMethodUser == null
+ || editorInfo.targetInputMethodUser.getIdentifier() != userId) {
+ throw new InvalidParameterException("EditorInfo#targetInputMethodUser must also be "
+ + "specified for cross-user startInputOrWindowGainedFocus()");
+ }
+ }
+
+ if (windowToken == null) {
+ Slog.e(TAG, "windowToken cannot be null.");
+ return InputBindResult.NULL;
+ }
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+ "IMMS.startInputOrWindowGainedFocus");
+ ImeTracing.getInstance().triggerManagerServiceDump(
+ "InputMethodManagerService#startInputOrWindowGainedFocus");
+ final InputBindResult result;
+ synchronized (ImfLock.class) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ result = startInputOrWindowGainedFocusInternalLocked(startInputReason,
+ client, windowToken, startInputFlags, softInputMode, windowFlags,
+ editorInfo, inputConnection, remoteAccessibilityInputConnection,
+ unverifiedTargetSdkVersion, userId, imeDispatcher);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ if (result == null) {
+ // This must never happen, but just in case.
+ Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason="
+ + InputMethodDebug.startInputReasonToString(startInputReason)
+ + " windowFlags=#" + Integer.toHexString(windowFlags)
+ + " editorInfo=" + editorInfo);
+ return InputBindResult.NULL;
+ }
+
+ return result;
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private InputBindResult startInputOrWindowGainedFocusInternalLocked(
+ @StartInputReason int startInputReason, IInputMethodClient client,
+ @NonNull IBinder windowToken, @StartInputFlags int startInputFlags,
+ @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo editorInfo,
+ IRemoteInputConnection inputContext,
+ @Nullable IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
+ int unverifiedTargetSdkVersion, @UserIdInt int userId,
+ @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+ if (DEBUG) {
+ Slog.v(TAG, "startInputOrWindowGainedFocusInternalLocked: reason="
+ + InputMethodDebug.startInputReasonToString(startInputReason)
+ + " client=" + client.asBinder()
+ + " inputContext=" + inputContext
+ + " editorInfo=" + editorInfo
+ + " startInputFlags="
+ + InputMethodDebug.startInputFlagsToString(startInputFlags)
+ + " softInputMode=" + InputMethodDebug.softInputModeToString(softInputMode)
+ + " windowFlags=#" + Integer.toHexString(windowFlags)
+ + " unverifiedTargetSdkVersion=" + unverifiedTargetSdkVersion
+ + " userId=" + userId
+ + " imeDispatcher=" + imeDispatcher);
+ }
+
+ if (!mUserManagerInternal.isUserRunning(userId)) {
+ // There is a chance that we hit here because of race condition. Let's just
+ // return an error code instead of crashing the caller process, which at
+ // least has INTERACT_ACROSS_USERS_FULL permission thus is likely to be an
+ // important process.
+ Slog.w(TAG, "User #" + userId + " is not running.");
+ return InputBindResult.INVALID_USER;
+ }
+
+ final ClientState cs = mClients.get(client.asBinder());
+ if (cs == null) {
+ throw new IllegalArgumentException("unknown client " + client.asBinder());
+ }
+
+ final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus(
+ windowToken, cs.mUid, cs.mPid, cs.mSelfReportedDisplayId);
+ switch (imeClientFocus) {
+ case WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH:
+ Slog.e(TAG, "startInputOrWindowGainedFocusInternal: display ID mismatch.");
+ return InputBindResult.DISPLAY_ID_MISMATCH;
+ case WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW:
+ // Check with the window manager to make sure this client actually
+ // has a window with focus. If not, reject. This is thread safe
+ // because if the focus changes some time before or after, the
+ // next client receiving focus that has any interest in input will
+ // be calling through here after that change happens.
+ if (DEBUG) {
+ Slog.w(TAG, "Focus gain on non-focused client " + cs.mClient
+ + " (uid=" + cs.mUid + " pid=" + cs.mPid + ")");
+ }
+ return InputBindResult.NOT_IME_TARGET_WINDOW;
+ case WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID:
+ return InputBindResult.INVALID_DISPLAY_ID;
+ }
+
+ if (mUserSwitchHandlerTask != null) {
+ // There is already an on-going pending user switch task.
+ final int nextUserId = mUserSwitchHandlerTask.mToUserId;
+ if (userId == nextUserId) {
+ scheduleSwitchUserTaskLocked(userId, cs.mClient);
+ return InputBindResult.USER_SWITCHING;
+ }
+ final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds(
+ mSettings.getCurrentUserId(), false /* enabledOnly */);
+ for (int profileId : profileIdsWithDisabled) {
+ if (profileId == userId) {
+ scheduleSwitchUserTaskLocked(userId, cs.mClient);
+ return InputBindResult.USER_SWITCHING;
+ }
+ }
+ return InputBindResult.INVALID_USER;
+ }
+
+ final boolean shouldClearFlag = mImePlatformCompatUtils.shouldClearShowForcedFlag(cs.mUid);
+ // In case mShowForced flag affects the next client to keep IME visible, when the current
+ // client is leaving due to the next focused client, we clear mShowForced flag when the
+ // next client's targetSdkVersion is T or higher.
+ final boolean showForced = mVisibilityStateComputer.mShowForced;
+ if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) {
+ mVisibilityStateComputer.mShowForced = false;
+ }
+
+ // cross-profile access is always allowed here to allow profile-switching.
+ if (!mSettings.isCurrentProfile(userId)) {
+ Slog.w(TAG, "A background user is requesting window. Hiding IME.");
+ Slog.w(TAG, "If you need to impersonate a foreground user/profile from"
+ + " a background user, use EditorInfo.targetInputMethodUser with"
+ + " INTERACT_ACROSS_USERS_FULL permission.");
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, SoftInputShowHideReason.HIDE_INVALID_USER);
+ return InputBindResult.INVALID_USER;
+ }
+
+ if (userId != mSettings.getCurrentUserId()) {
+ scheduleSwitchUserTaskLocked(userId, cs.mClient);
+ return InputBindResult.USER_SWITCHING;
+ }
+
+ final boolean sameWindowFocused = mCurFocusedWindow == windowToken;
+ final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
+ final boolean startInputByWinGainedFocus =
+ (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0;
+
+ // Init the focused window state (e.g. whether the editor has focused or IME focus has
+ // changed from another window).
+ final ImeTargetWindowState windowState = new ImeTargetWindowState(softInputMode,
+ windowFlags, !sameWindowFocused, isTextEditor, startInputByWinGainedFocus);
+ mVisibilityStateComputer.setWindowState(windowToken, windowState);
+
+ if (sameWindowFocused && isTextEditor) {
+ if (DEBUG) {
+ Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
+ + " editorInfo=" + editorInfo + ", token = " + windowToken
+ + ", startInputReason="
+ + InputMethodDebug.startInputReasonToString(startInputReason));
+ }
+ if (editorInfo != null) {
+ return startInputUncheckedLocked(cs, inputContext,
+ remoteAccessibilityInputConnection, editorInfo, startInputFlags,
+ startInputReason, unverifiedTargetSdkVersion, imeDispatcher);
+ }
+ return new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
+ null, null, null, null, -1, null, false);
+ }
+
+ mCurFocusedWindow = windowToken;
+ mCurFocusedWindowSoftInputMode = softInputMode;
+ mCurFocusedWindowClient = cs;
+ mCurFocusedWindowEditorInfo = editorInfo;
+ mCurPerceptible = true;
+
+ // We want to start input before showing the IME, but after closing
+ // it. We want to do this after closing it to help the IME disappear
+ // more quickly (not get stuck behind it initializing itself for the
+ // new focused input, even if its window wants to hide the IME).
+ boolean didStart = false;
+ InputBindResult res = null;
+
+ final ImeVisibilityResult imeVisRes = mVisibilityStateComputer.computeState(windowState,
+ isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags));
+ if (imeVisRes != null) {
+ switch (imeVisRes.getReason()) {
+ case SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY:
+ case SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV:
+ case SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV:
+ case SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE:
+ if (editorInfo != null) {
+ res = startInputUncheckedLocked(cs, inputContext,
+ remoteAccessibilityInputConnection, editorInfo, startInputFlags,
+ startInputReason, unverifiedTargetSdkVersion,
+ imeDispatcher);
+ didStart = true;
+ }
+ break;
+ }
+
+ mVisibilityApplier.applyImeVisibility(mCurFocusedWindow, null /* statsToken */,
+ imeVisRes.getState(), imeVisRes.getReason());
+
+ if (imeVisRes.getReason() == SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW) {
+ // If focused display changed, we should unbind current method
+ // to make app window in previous display relayout after Ime
+ // window token removed.
+ // Note that we can trust client's display ID as long as it matches
+ // to the display ID obtained from the window.
+ if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
+ mBindingController.unbindCurrentMethod();
+ }
+ }
+ }
+ if (!didStart) {
+ if (editorInfo != null) {
+ res = startInputUncheckedLocked(cs, inputContext,
+ remoteAccessibilityInputConnection, editorInfo, startInputFlags,
+ startInputReason, unverifiedTargetSdkVersion,
+ imeDispatcher);
+ } else {
+ res = InputBindResult.NULL_EDITOR_INFO;
+ }
+ }
+ return res;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void showCurrentInputImplicitLocked(@NonNull IBinder windowToken,
+ @SoftInputShowHideReason int reason) {
+ showCurrentInputLocked(windowToken, null /* statsToken */, InputMethodManager.SHOW_IMPLICIT,
+ null /* resultReceiver */, reason);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
+ @Nullable ImeTracker.Token statsToken) {
+ if (mCurClient == null || client == null
+ || mCurClient.mClient.asBinder() != client.asBinder()) {
+ // We need to check if this is the current client with
+ // focus in the window manager, to allow this call to
+ // be made before input is started in it.
+ final ClientState cs = mClients.get(client.asBinder());
+ if (cs == null) {
+ ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
+ throw new IllegalArgumentException("unknown client " + client.asBinder());
+ }
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
+ if (!isImeClientFocused(mCurFocusedWindow, cs)) {
+ Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
+ return false;
+ }
+ }
+ ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
+ return true;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean canShowInputMethodPickerLocked(IInputMethodClient client) {
+ final int uid = Binder.getCallingUid();
+ if (mCurFocusedWindowClient != null && client != null
+ && mCurFocusedWindowClient.mClient.asBinder() == client.asBinder()) {
+ return true;
+ }
+ if (mSettings.getCurrentUserId() != UserHandle.getUserId(uid)) {
+ return false;
+ }
+ if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal,
+ uid,
+ getCurIntentLocked().getComponent().getPackageName())) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void showInputMethodPickerFromClient(IInputMethodClient client,
+ int auxiliarySubtypeMode) {
+ synchronized (ImfLock.class) {
+ if (!canShowInputMethodPickerLocked(client)) {
+ Slog.w(TAG, "Ignoring showInputMethodPickerFromClient of uid "
+ + Binder.getCallingUid() + ": " + client);
+ return;
+ }
+
+ // Always call subtype picker, because subtype picker is a superset of input method
+ // picker.
+ final int displayId =
+ (mCurClient != null) ? mCurClient.mSelfReportedDisplayId : DEFAULT_DISPLAY;
+ mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
+ .sendToTarget();
+ }
+ }
+
+ @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ @Override
+ public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
+ // Always call subtype picker, because subtype picker is a superset of input method
+ // picker.
+ super.showInputMethodPickerFromSystem_enforcePermission();
+
+ mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
+ .sendToTarget();
+ }
+
+ /**
+ * A test API for CTS to make sure that the input method menu is showing.
+ */
+ @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
+ public boolean isInputMethodPickerShownForTest() {
+ super.isInputMethodPickerShownForTest_enforcePermission();
+
+ synchronized (ImfLock.class) {
+ return mMenuController.isisInputMethodPickerShownForTestLocked();
+ }
+ }
+
+ @NonNull
+ private static IllegalArgumentException getExceptionForUnknownImeId(
+ @Nullable String imeId) {
+ return new IllegalArgumentException("Unknown id: " + imeId);
+ }
+
+ @BinderThread
+ private void setInputMethod(@NonNull IBinder token, String id) {
+ final int callingUid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(callingUid);
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final InputMethodInfo imi = mMethodMap.get(id);
+ if (imi == null || !canCallerAccessInputMethod(
+ imi.getPackageName(), callingUid, userId, mSettings)) {
+ throw getExceptionForUnknownImeId(id);
+ }
+ setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID);
+ }
+ }
+
+ @BinderThread
+ private void setInputMethodAndSubtype(@NonNull IBinder token, String id,
+ InputMethodSubtype subtype) {
+ final int callingUid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(callingUid);
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final InputMethodInfo imi = mMethodMap.get(id);
+ if (imi == null || !canCallerAccessInputMethod(
+ imi.getPackageName(), callingUid, userId, mSettings)) {
+ throw getExceptionForUnknownImeId(id);
+ }
+ if (subtype != null) {
+ setInputMethodWithSubtypeIdLocked(token, id,
+ SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode()));
+ } else {
+ setInputMethod(token, id);
+ }
+ }
+ }
+
+ @BinderThread
+ private boolean switchToPreviousInputMethod(@NonNull IBinder token) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return false;
+ }
+ final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
+ final InputMethodInfo lastImi;
+ if (lastIme != null) {
+ lastImi = mMethodMap.get(lastIme.first);
+ } else {
+ lastImi = null;
+ }
+ String targetLastImiId = null;
+ int subtypeId = NOT_A_SUBTYPE_ID;
+ if (lastIme != null && lastImi != null) {
+ final boolean imiIdIsSame = lastImi.getId().equals(getSelectedMethodIdLocked());
+ final int lastSubtypeHash = Integer.parseInt(lastIme.second);
+ final int currentSubtypeHash = mCurrentSubtype == null ? NOT_A_SUBTYPE_ID
+ : mCurrentSubtype.hashCode();
+ // If the last IME is the same as the current IME and the last subtype is not
+ // defined, there is no need to switch to the last IME.
+ if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) {
+ targetLastImiId = lastIme.first;
+ subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash);
+ }
+ }
+
+ if (TextUtils.isEmpty(targetLastImiId)
+ && !InputMethodUtils.canAddToLastInputMethod(mCurrentSubtype)) {
+ // This is a safety net. If the currentSubtype can't be added to the history
+ // and the framework couldn't find the last ime, we will make the last ime be
+ // the most applicable enabled keyboard subtype of the system imes.
+ final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ if (enabled != null) {
+ final int enabledCount = enabled.size();
+ final String locale = mCurrentSubtype == null
+ ? mRes.getConfiguration().locale.toString()
+ : mCurrentSubtype.getLocale();
+ for (int i = 0; i < enabledCount; ++i) {
+ final InputMethodInfo imi = enabled.get(i);
+ if (imi.getSubtypeCount() > 0 && imi.isSystem()) {
+ InputMethodSubtype keyboardSubtype =
+ SubtypeUtils.findLastResortApplicableSubtypeLocked(mRes,
+ SubtypeUtils.getSubtypes(imi),
+ SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
+ if (keyboardSubtype != null) {
+ targetLastImiId = imi.getId();
+ subtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi,
+ keyboardSubtype.hashCode());
+ if (keyboardSubtype.getLocale().equals(locale)) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!TextUtils.isEmpty(targetLastImiId)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second
+ + ", from: " + getSelectedMethodIdLocked() + ", " + subtypeId);
+ }
+ setInputMethodWithSubtypeIdLocked(token, targetLastImiId, subtypeId);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @BinderThread
+ private boolean switchToNextInputMethod(@NonNull IBinder token, boolean onlyCurrentIme) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return false;
+ }
+ return switchToNextInputMethodLocked(token, onlyCurrentIme);
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) {
+ final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
+ onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype);
+ if (nextSubtype == null) {
+ return false;
+ }
+ setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(),
+ nextSubtype.mSubtypeId);
+ return true;
+ }
+
+ @BinderThread
+ private boolean shouldOfferSwitchingToNextInputMethod(@NonNull IBinder token) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return false;
+ }
+ final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
+ false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()),
+ mCurrentSubtype);
+ return nextSubtype != null;
+ }
+ }
+
+ @Override
+ public InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ synchronized (ImfLock.class) {
+ if (mSettings.getCurrentUserId() == userId) {
+ return mSettings.getLastInputMethodSubtypeLocked();
+ }
+
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
+ return settings.getLastInputMethodSubtypeLocked();
+ }
+ }
+
+ @Override
+ public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes,
+ @UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ final int callingUid = Binder.getCallingUid();
+
+ // By this IPC call, only a process which shares the same uid with the IME can add
+ // additional input method subtypes to the IME.
+ if (TextUtils.isEmpty(imiId) || subtypes == null) return;
+ final ArrayList<InputMethodSubtype> toBeAdded = new ArrayList<>();
+ for (InputMethodSubtype subtype : subtypes) {
+ if (!toBeAdded.contains(subtype)) {
+ toBeAdded.add(subtype);
+ } else {
+ Slog.w(TAG, "Duplicated subtype definition found: "
+ + subtype.getLocale() + ", " + subtype.getMode());
+ }
+ }
+ synchronized (ImfLock.class) {
+ if (!mSystemReady) {
+ return;
+ }
+
+ if (mSettings.getCurrentUserId() == userId) {
+ if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
+ mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return;
+ }
+
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+ new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
+ methodList, DirectBootAwareness.AUTO, mSettings.getEnabledInputMethodNames());
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
+ settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
+ mPackageManagerInternal, callingUid);
+ }
+ }
+
+ @Override
+ public void setExplicitlyEnabledInputMethodSubtypes(String imeId,
+ @NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final ComponentName imeComponentName =
+ imeId != null ? ComponentName.unflattenFromString(imeId) : null;
+ if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(
+ mPackageManagerInternal, callingUid, imeComponentName.getPackageName())) {
+ throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId="
+ + imeId);
+ }
+ Objects.requireNonNull(subtypeHashCodes, "subtypeHashCodes must not be null");
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (ImfLock.class) {
+ final boolean currentUser = (mSettings.getCurrentUserId() == userId);
+ final InputMethodSettings settings = currentUser
+ ? mSettings
+ : new InputMethodSettings(mContext, queryMethodMapForUser(userId), userId,
+ !mUserManagerInternal.isUserUnlocked(userId));
+ if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) {
+ return;
+ }
+ if (currentUser) {
+ // To avoid unnecessary "updateInputMethodsFromSettingsLocked" from happening.
+ if (mSettingsObserver != null) {
+ mSettingsObserver.mLastEnabled = settings.getEnabledInputMethodsStr();
+ }
+ updateInputMethodsFromSettingsLocked(false /* enabledChanged */);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * This is kept due to {@code @UnsupportedAppUsage} in
+ * {@link InputMethodManager#getInputMethodWindowVisibleHeight()} and a dependency in
+ * {@link InputMethodService#onCreate()}.
+ *
+ * @return {@link WindowManagerInternal#getInputMethodWindowVisibleHeight(int)}
+ *
+ * @deprecated TODO(b/113914148): Check if we can remove this
+ */
+ @Override
+ @Deprecated
+ public int getInputMethodWindowVisibleHeight(@NonNull IInputMethodClient client) {
+ int callingUid = Binder.getCallingUid();
+ return Binder.withCleanCallingIdentity(() -> {
+ final int curTokenDisplayId;
+ synchronized (ImfLock.class) {
+ if (!canInteractWithImeLocked(callingUid, client,
+ "getInputMethodWindowVisibleHeight", null /* statsToken */)) {
+ if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) {
+ EventLog.writeEvent(0x534e4554, "204906124", callingUid, "");
+ mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true);
+ }
+ return 0;
+ }
+ // This should probably use the caller's display id, but because this is unsupported
+ // and maintained only for compatibility, there's no point in fixing it.
+ curTokenDisplayId = mCurTokenDisplayId;
+ }
+ return mWindowManagerInternal.getInputMethodWindowVisibleHeight(curTokenDisplayId);
+ });
+ }
+
+ @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+ @Override
+ public void removeImeSurface() {
+ super.removeImeSurface_enforcePermission();
+
+ mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
+ }
+
+ @Override
+ public void reportVirtualDisplayGeometryAsync(IInputMethodClient parentClient,
+ int childDisplayId, float[] matrixValues) {
+ final IInputMethodClientInvoker parentClientInvoker =
+ IInputMethodClientInvoker.create(parentClient, mHandler);
+ try {
+ final DisplayInfo displayInfo = mDisplayManagerInternal.getDisplayInfo(childDisplayId);
+ if (displayInfo == null) {
+ throw new IllegalArgumentException(
+ "Cannot find display for non-existent displayId: " + childDisplayId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != displayInfo.ownerUid) {
+ throw new SecurityException("The caller doesn't own the display.");
+ }
+
+ synchronized (ImfLock.class) {
+ final ClientState cs = mClients.get(parentClientInvoker.asBinder());
+ if (cs == null) {
+ return;
+ }
+
+ // null matrixValues means that the entry needs to be removed.
+ if (matrixValues == null) {
+ final VirtualDisplayInfo info =
+ mVirtualDisplayIdToParentMap.get(childDisplayId);
+ if (info == null) {
+ return;
+ }
+ if (info.mParentClient != cs) {
+ throw new SecurityException("Only the owner client can clear"
+ + " VirtualDisplayGeometry for display #" + childDisplayId);
+ }
+ mVirtualDisplayIdToParentMap.remove(childDisplayId);
+ return;
+ }
+
+ VirtualDisplayInfo info = mVirtualDisplayIdToParentMap.get(childDisplayId);
+ if (info != null && info.mParentClient != cs) {
+ throw new InvalidParameterException("Display #" + childDisplayId
+ + " is already registered by " + info.mParentClient);
+ }
+ if (info == null) {
+ if (!mWindowManagerInternal.isUidAllowedOnDisplay(childDisplayId, cs.mUid)) {
+ throw new SecurityException(cs + " cannot access to display #"
+ + childDisplayId);
+ }
+ info = new VirtualDisplayInfo(cs, new Matrix());
+ mVirtualDisplayIdToParentMap.put(childDisplayId, info);
+ }
+ info.mMatrix.setValues(matrixValues);
+
+ if (mCurClient == null || mCurClient.mCurSession == null) {
+ return;
+ }
+
+ Matrix matrix = null;
+ int displayId = mCurClient.mSelfReportedDisplayId;
+ boolean needToNotify = false;
+ while (true) {
+ needToNotify |= (displayId == childDisplayId);
+ final VirtualDisplayInfo next = mVirtualDisplayIdToParentMap.get(displayId);
+ if (next == null) {
+ break;
+ }
+ if (matrix == null) {
+ matrix = new Matrix(next.mMatrix);
+ } else {
+ matrix.postConcat(next.mMatrix);
+ }
+ if (next.mParentClient.mSelfReportedDisplayId == mCurTokenDisplayId) {
+ if (needToNotify) {
+ final float[] values = new float[9];
+ matrix.getValues(values);
+ mCurClient.mClient.updateVirtualDisplayToScreenMatrix(
+ getSequenceNumberLocked(), values);
+ }
+ break;
+ }
+ displayId = info.mParentClient.mSelfReportedDisplayId;
+ }
+ }
+ } catch (Throwable t) {
+ if (parentClientInvoker != null) {
+ parentClientInvoker.throwExceptionFromSystem(t.toString());
+ }
+ }
+ }
+
+ @Override
+ public void removeImeSurfaceFromWindowAsync(IBinder windowToken) {
+ // No permission check, because we'll only execute the request if the calling window is
+ // also the current IME client.
+ mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE_FROM_WINDOW, windowToken).sendToTarget();
+ }
+
+ private void registerDeviceListenerAndCheckStylusSupport() {
+ final InputManager im = mContext.getSystemService(InputManager.class);
+ final IntArray stylusIds = getStylusInputDeviceIds(im);
+ if (stylusIds.size() > 0) {
+ synchronized (ImfLock.class) {
+ mStylusIds = new IntArray();
+ mStylusIds.addAll(stylusIds);
+ }
+ }
+ im.registerInputDeviceListener(new InputManager.InputDeviceListener() {
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ InputDevice device = im.getInputDevice(deviceId);
+ if (device != null && isStylusDevice(device)) {
+ add(deviceId);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ remove(deviceId);
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ InputDevice device = im.getInputDevice(deviceId);
+ if (device == null) {
+ return;
+ }
+ if (isStylusDevice(device)) {
+ add(deviceId);
+ } else {
+ remove(deviceId);
+ }
+ }
+
+ private void add(int deviceId) {
+ synchronized (ImfLock.class) {
+ addStylusDeviceIdLocked(deviceId);
+ }
+ }
+
+ private void remove(int deviceId) {
+ synchronized (ImfLock.class) {
+ removeStylusDeviceIdLocked(deviceId);
+ }
+ }
+ }, mHandler);
+ }
+
+ private void addStylusDeviceIdLocked(int deviceId) {
+ if (mStylusIds == null) {
+ mStylusIds = new IntArray();
+ } else if (mStylusIds.indexOf(deviceId) != -1) {
+ return;
+ }
+ Slog.d(TAG, "New Stylus deviceId" + deviceId + " added.");
+ mStylusIds.add(deviceId);
+ // a new Stylus is detected. If IME supports handwriting, and we don't have
+ // handwriting initialized, lets do it now.
+ if (!mHwController.getCurrentRequestId().isPresent()
+ && mBindingController.supportsStylusHandwriting()) {
+ scheduleResetStylusHandwriting();
+ }
+ }
+
+ private void removeStylusDeviceIdLocked(int deviceId) {
+ if (mStylusIds == null || mStylusIds.size() == 0) {
+ return;
+ }
+ int index;
+ if ((index = mStylusIds.indexOf(deviceId)) != -1) {
+ mStylusIds.remove(index);
+ Slog.d(TAG, "Stylus deviceId: " + deviceId + " removed.");
+ }
+ if (mStylusIds.size() == 0) {
+ // no more supported stylus(es) in system.
+ mHwController.reset();
+ scheduleRemoveStylusHandwritingWindow();
+ }
+ }
+
+ private static boolean isStylusDevice(InputDevice inputDevice) {
+ return inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)
+ || inputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS);
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean hasSupportedStylusLocked() {
+ return mStylusIds != null && mStylusIds.size() != 0;
+ }
+
+ /**
+ * Helper method that adds a virtual stylus id for next handwriting session test if
+ * a stylus deviceId is not already registered on device.
+ */
+ @BinderThread
+ @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
+ @Override
+ public void addVirtualStylusIdForTestSession(IInputMethodClient client) {
+ super.addVirtualStylusIdForTestSession_enforcePermission();
+
+ int uid = Binder.getCallingUid();
+ synchronized (ImfLock.class) {
+ if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession",
+ null /* statsToken */)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (DEBUG) Slog.v(TAG, "Adding virtual stylus id for session");
+ addStylusDeviceIdLocked(VIRTUAL_STYLUS_ID_FOR_TEST);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ /**
+ * Helper method to set a stylus idle-timeout after which handwriting {@code InkWindow}
+ * will be removed.
+ * @param timeout to set in milliseconds. To reset to default, use a value <= zero.
+ */
+ @BinderThread
+ @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
+ @Override
+ public void setStylusWindowIdleTimeoutForTest(
+ IInputMethodClient client, @DurationMillisLong long timeout) {
+ super.setStylusWindowIdleTimeoutForTest_enforcePermission();
+
+ int uid = Binder.getCallingUid();
+ synchronized (ImfLock.class) {
+ if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest",
+ null /* statsToken */)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (DEBUG) Slog.v(TAG, "Setting stylus window idle timeout");
+ getCurMethodLocked().setStylusWindowIdleTimeoutForTest(timeout);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void removeVirtualStylusIdForTestSessionLocked() {
+ removeStylusDeviceIdLocked(VIRTUAL_STYLUS_ID_FOR_TEST);
+ }
+
+ private static IntArray getStylusInputDeviceIds(InputManager im) {
+ IntArray stylusIds = new IntArray();
+ for (int id : im.getInputDeviceIds()) {
+ if (!im.isInputDeviceEnabled(id)) {
+ continue;
+ }
+ InputDevice device = im.getInputDevice(id);
+ if (device != null && isStylusDevice(device)) {
+ stylusIds.add(id);
+ }
+ }
+
+ return stylusIds;
+ }
+
+ /**
+ * Starting point for dumping the IME tracing information in proto format.
+ *
+ * @param protoDump dump information from the IME client side
+ */
+ @BinderThread
+ @Override
+ public void startProtoDump(byte[] protoDump, int source, String where) {
+ if (protoDump == null && source != ImeTracing.IME_TRACING_FROM_IMMS) {
+ // Dump not triggered from IMMS, but no proto information provided.
+ return;
+ }
+ ImeTracing tracingInstance = ImeTracing.getInstance();
+ if (!tracingInstance.isAvailable() || !tracingInstance.isEnabled()) {
+ return;
+ }
+
+ ProtoOutputStream proto = new ProtoOutputStream();
+ switch (source) {
+ case ImeTracing.IME_TRACING_FROM_CLIENT:
+ final long client_token = proto.start(InputMethodClientsTraceFileProto.ENTRY);
+ proto.write(InputMethodClientsTraceProto.ELAPSED_REALTIME_NANOS,
+ SystemClock.elapsedRealtimeNanos());
+ proto.write(InputMethodClientsTraceProto.WHERE, where);
+ proto.write(InputMethodClientsTraceProto.CLIENT, protoDump);
+ proto.end(client_token);
+ break;
+ case ImeTracing.IME_TRACING_FROM_IMS:
+ final long service_token = proto.start(InputMethodServiceTraceFileProto.ENTRY);
+ proto.write(InputMethodServiceTraceProto.ELAPSED_REALTIME_NANOS,
+ SystemClock.elapsedRealtimeNanos());
+ proto.write(InputMethodServiceTraceProto.WHERE, where);
+ proto.write(InputMethodServiceTraceProto.INPUT_METHOD_SERVICE, protoDump);
+ proto.end(service_token);
+ break;
+ case ImeTracing.IME_TRACING_FROM_IMMS:
+ final long managerservice_token =
+ proto.start(InputMethodManagerServiceTraceFileProto.ENTRY);
+ proto.write(InputMethodManagerServiceTraceProto.ELAPSED_REALTIME_NANOS,
+ SystemClock.elapsedRealtimeNanos());
+ proto.write(InputMethodManagerServiceTraceProto.WHERE, where);
+ dumpDebug(proto,
+ InputMethodManagerServiceTraceProto.INPUT_METHOD_MANAGER_SERVICE);
+ proto.end(managerservice_token);
+ break;
+ default:
+ // Dump triggered by a source not recognised.
+ return;
+ }
+ tracingInstance.addToBuffer(proto, source);
+ }
+
+ @BinderThread
+ @Override
+ public boolean isImeTraceEnabled() {
+ return ImeTracing.getInstance().isEnabled();
+ }
+
+ @BinderThread
+ @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING)
+ @Override
+ public void startImeTrace() {
+ super.startImeTrace_enforcePermission();
+
+ ImeTracing.getInstance().startTrace(null /* printwriter */);
+ ArrayMap<IBinder, ClientState> clients;
+ synchronized (ImfLock.class) {
+ clients = new ArrayMap<>(mClients);
+ }
+ for (ClientState state : clients.values()) {
+ if (state != null) {
+ state.mClient.setImeTraceEnabled(true /* enabled */);
+ }
+ }
+ }
+
+ @BinderThread
+ @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING)
+ @Override
+ public void stopImeTrace() {
+ super.stopImeTrace_enforcePermission();
+
+ ImeTracing.getInstance().stopTrace(null /* printwriter */);
+ ArrayMap<IBinder, ClientState> clients;
+ synchronized (ImfLock.class) {
+ clients = new ArrayMap<>(mClients);
+ }
+ for (ClientState state : clients.values()) {
+ if (state != null) {
+ state.mClient.setImeTraceEnabled(false /* enabled */);
+ }
+ }
+ }
+
+ private void dumpDebug(ProtoOutputStream proto, long fieldId) {
+ synchronized (ImfLock.class) {
+ final long token = proto.start(fieldId);
+ proto.write(CUR_METHOD_ID, getSelectedMethodIdLocked());
+ proto.write(CUR_SEQ, getSequenceNumberLocked());
+ proto.write(CUR_CLIENT, Objects.toString(mCurClient));
+ proto.write(CUR_FOCUSED_WINDOW_NAME,
+ mWindowManagerInternal.getWindowName(mCurFocusedWindow));
+ proto.write(LAST_IME_TARGET_WINDOW_NAME,
+ mWindowManagerInternal.getWindowName(mLastImeTargetWindow));
+ proto.write(CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE,
+ InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode));
+ if (mCurEditorInfo != null) {
+ mCurEditorInfo.dumpDebug(proto, CUR_ATTRIBUTE);
+ }
+ proto.write(CUR_ID, getCurIdLocked());
+ mVisibilityStateComputer.dumpDebug(proto, fieldId);
+ proto.write(IN_FULLSCREEN_MODE, mInFullscreenMode);
+ proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked()));
+ proto.write(CUR_TOKEN_DISPLAY_ID, mCurTokenDisplayId);
+ proto.write(SYSTEM_READY, mSystemReady);
+ proto.write(LAST_SWITCH_USER_ID, mLastSwitchUserId);
+ proto.write(HAVE_CONNECTION, hasConnectionLocked());
+ proto.write(BOUND_TO_METHOD, mBoundToMethod);
+ proto.write(IS_INTERACTIVE, mIsInteractive);
+ proto.write(BACK_DISPOSITION, mBackDisposition);
+ proto.write(IME_WINDOW_VISIBILITY, mImeWindowVis);
+ proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard());
+ proto.end(token);
+ }
+ }
+
+ @BinderThread
+ private void notifyUserAction(@NonNull IBinder token) {
+ if (DEBUG) {
+ Slog.d(TAG, "Got the notification of a user action.");
+ }
+ synchronized (ImfLock.class) {
+ if (getCurTokenLocked() != token) {
+ if (DEBUG) {
+ Slog.d(TAG, "Ignoring the user action notification from IMEs that are no longer"
+ + " active.");
+ }
+ return;
+ }
+ final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
+ if (imi != null) {
+ mSwitchingController.onUserActionLocked(imi, mCurrentSubtype);
+ }
+ }
+ }
+
+ @BinderThread
+ private void applyImeVisibility(IBinder token, IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility");
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY);
+ return;
+ }
+ final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(
+ windowToken);
+ mVisibilityApplier.applyImeVisibility(requestToken, statsToken,
+ setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME
+ : ImeVisibilityStateComputer.STATE_HIDE_IME);
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @BinderThread
+ private void resetStylusHandwriting(int requestId) {
+ synchronized (ImfLock.class) {
+ final OptionalInt curRequest = mHwController.getCurrentRequestId();
+ if (!curRequest.isPresent() || curRequest.getAsInt() != requestId) {
+ Slog.w(TAG, "IME requested to finish handwriting with a mismatched requestId: "
+ + requestId);
+ }
+ removeVirtualStylusIdForTestSessionLocked();
+ scheduleResetStylusHandwriting();
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void setInputMethodWithSubtypeIdLocked(IBinder token, String id, int subtypeId) {
+ if (token == null) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Using null token requires permission "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ }
+ } else if (getCurTokenLocked() != token) {
+ Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid()
+ + " token: " + token);
+ return;
+ } else {
+ // Called with current IME's token.
+ if (mMethodMap.get(id) != null
+ && mSettings.getEnabledInputMethodListWithFilterLocked(
+ (info) -> info.getId().equals(id)).isEmpty()) {
+ throw new IllegalStateException("Requested IME is not enabled: " + id);
+ }
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ setInputMethodLocked(id, subtypeId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * Called right after {@link IInputMethod#showSoftInput} or {@link IInputMethod#hideSoftInput}.
+ */
+ @GuardedBy("ImfLock.class")
+ void onShowHideSoftInputRequested(boolean show, IBinder requestImeToken,
+ @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) {
+ final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken);
+ final WindowManagerInternal.ImeTargetInfo info =
+ mWindowManagerInternal.onToggleImeRequested(
+ show, mCurFocusedWindow, requestToken, mCurTokenDisplayId);
+ mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
+ mCurFocusedWindowClient, mCurFocusedWindowEditorInfo, info.focusedWindowName,
+ mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode,
+ info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName,
+ info.imeSurfaceParentName));
+
+ if (statsToken != null) {
+ mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
+ }
+ }
+
+ @BinderThread
+ private void hideMySoftInput(@NonNull IBinder token, int flags,
+ @SoftInputShowHideReason int reason) {
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ hideCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */, reason);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @BinderThread
+ private void showMySoftInput(@NonNull IBinder token, int flags) {
+ try {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ showCurrentInputLocked(mLastImeTargetWindow, null /* statsToken */, flags,
+ null /* resultReceiver */,
+ SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ }
+ }
+
+ @VisibleForTesting
+ ImeVisibilityApplier getVisibilityApplier() {
+ synchronized (ImfLock.class) {
+ return mVisibilityApplier;
+ }
+ }
+
+ void onApplyImeVisibilityFromComputer(IBinder windowToken,
+ @NonNull ImeVisibilityResult result) {
+ synchronized (ImfLock.class) {
+ mVisibilityApplier.applyImeVisibility(windowToken, null, result.getState(),
+ result.getReason());
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void setEnabledSessionLocked(SessionState session) {
+ if (mEnabledSession != session) {
+ if (mEnabledSession != null && mEnabledSession.mSession != null) {
+ if (DEBUG) Slog.v(TAG, "Disabling: " + mEnabledSession);
+ mEnabledSession.mMethod.setSessionEnabled(mEnabledSession.mSession, false);
+ }
+ mEnabledSession = session;
+ if (mEnabledSession != null && mEnabledSession.mSession != null) {
+ if (DEBUG) Slog.v(TAG, "Enabling: " + mEnabledSession);
+ mEnabledSession.mMethod.setSessionEnabled(mEnabledSession.mSession, true);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void setEnabledSessionForAccessibilityLocked(
+ SparseArray<AccessibilitySessionState> accessibilitySessions) {
+ // mEnabledAccessibilitySessions could the same object as accessibilitySessions.
+ SparseArray<IAccessibilityInputMethodSession> disabledSessions = new SparseArray<>();
+ for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
+ if (!accessibilitySessions.contains(mEnabledAccessibilitySessions.keyAt(i))) {
+ AccessibilitySessionState sessionState = mEnabledAccessibilitySessions.valueAt(i);
+ if (sessionState != null) {
+ disabledSessions.append(mEnabledAccessibilitySessions.keyAt(i),
+ sessionState.mSession);
+ }
+ }
+ }
+ if (disabledSessions.size() > 0) {
+ AccessibilityManagerInternal.get().setImeSessionEnabled(disabledSessions,
+ false);
+ }
+ SparseArray<IAccessibilityInputMethodSession> enabledSessions = new SparseArray<>();
+ for (int i = 0; i < accessibilitySessions.size(); i++) {
+ if (!mEnabledAccessibilitySessions.contains(accessibilitySessions.keyAt(i))) {
+ AccessibilitySessionState sessionState = accessibilitySessions.valueAt(i);
+ if (sessionState != null) {
+ enabledSessions.append(accessibilitySessions.keyAt(i), sessionState.mSession);
+ }
+ }
+ }
+ if (enabledSessions.size() > 0) {
+ AccessibilityManagerInternal.get().setImeSessionEnabled(enabledSessions,
+ true);
+ }
+ mEnabledAccessibilitySessions = accessibilitySessions;
+ }
+
+ @SuppressWarnings("unchecked")
+ @UiThread
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SHOW_IM_SUBTYPE_PICKER:
+ final boolean showAuxSubtypes;
+ final int displayId = msg.arg2;
+ switch (msg.arg1) {
+ case InputMethodManager.SHOW_IM_PICKER_MODE_AUTO:
+ // This is undocumented so far, but IMM#showInputMethodPicker() has been
+ // implemented so that auxiliary subtypes will be excluded when the soft
+ // keyboard is invisible.
+ synchronized (ImfLock.class) {
+ showAuxSubtypes = isInputShown();
+ }
+ break;
+ case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES:
+ showAuxSubtypes = true;
+ break;
+ case InputMethodManager.SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES:
+ showAuxSubtypes = false;
+ break;
+ default:
+ Slog.e(TAG, "Unknown subtype picker mode = " + msg.arg1);
+ return false;
+ }
+ mMenuController.showInputMethodMenu(showAuxSubtypes, displayId);
+ return true;
+
+ // ---------------------------------------------------------
+
+ case MSG_HIDE_CURRENT_INPUT_METHOD:
+ synchronized (ImfLock.class) {
+ final @SoftInputShowHideReason int reason = (int) msg.obj;
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */,
+ null /* resultReceiver */, reason);
+
+ }
+ return true;
+ case MSG_REMOVE_IME_SURFACE: {
+ synchronized (ImfLock.class) {
+ try {
+ if (mEnabledSession != null && mEnabledSession.mSession != null
+ && !isShowRequestedForCurrentWindow()) {
+ mEnabledSession.mSession.removeImeSurface();
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ return true;
+ }
+ case MSG_REMOVE_IME_SURFACE_FROM_WINDOW: {
+ IBinder windowToken = (IBinder) msg.obj;
+ synchronized (ImfLock.class) {
+ try {
+ if (windowToken == mCurFocusedWindow
+ && mEnabledSession != null && mEnabledSession.mSession != null) {
+ mEnabledSession.mSession.removeImeSurface();
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ return true;
+ }
+ case MSG_UPDATE_IME_WINDOW_STATUS: {
+ updateImeWindowStatus(msg.arg1 == 1);
+ return true;
+ }
+
+ // ---------------------------------------------------------
+
+ case MSG_SET_INTERACTIVE:
+ handleSetInteractive(msg.arg1 != 0);
+ return true;
+
+ // --------------------------------------------------------------
+ case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
+ mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+ synchronized (ImfLock.class) {
+ sendOnNavButtonFlagsChangedLocked();
+ }
+ return true;
+ case MSG_SYSTEM_UNLOCK_USER: {
+ final int userId = msg.arg1;
+ onUnlockUser(userId);
+ return true;
+ }
+ case MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED: {
+ final int userId = msg.arg1;
+ final List<InputMethodInfo> imes = (List<InputMethodInfo>) msg.obj;
+ mInputMethodListListeners.forEach(
+ listener -> listener.onInputMethodListUpdated(imes, userId));
+ return true;
+ }
+
+ // ---------------------------------------------------------------
+ case MSG_NOTIFY_IME_UID_TO_AUDIO_SERVICE: {
+ if (mAudioManagerInternal == null) {
+ mAudioManagerInternal = LocalServices.getService(AudioManagerInternal.class);
+ }
+ if (mAudioManagerInternal != null) {
+ mAudioManagerInternal.setInputMethodServiceUid(msg.arg1 /* uid */);
+ }
+ return true;
+ }
+
+ case MSG_RESET_HANDWRITING: {
+ synchronized (ImfLock.class) {
+ if (mBindingController.supportsStylusHandwriting()
+ && getCurMethodLocked() != null && hasSupportedStylusLocked()) {
+ Slog.d(TAG, "Initializing Handwriting Spy");
+ mHwController.initializeHandwritingSpy(mCurTokenDisplayId);
+ } else {
+ mHwController.reset();
+ }
+ }
+ return true;
+ }
+ case MSG_PREPARE_HANDWRITING_DELEGATION:
+ synchronized (ImfLock.class) {
+ String delegate = (String) ((Pair) msg.obj).first;
+ String delegator = (String) ((Pair) msg.obj).second;
+ mHwController.prepareStylusHandwritingDelegation(delegate, delegator);
+ }
+ return true;
+ case MSG_START_HANDWRITING:
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod == null || mCurFocusedWindow == null) {
+ return true;
+ }
+ final HandwritingModeController.HandwritingSession session =
+ mHwController.startHandwritingSession(
+ msg.arg1 /*requestId*/,
+ msg.arg2 /*pid*/,
+ mBindingController.getCurMethodUid(),
+ mCurFocusedWindow);
+ if (session == null) {
+ Slog.e(TAG,
+ "Failed to start handwriting session for requestId: " + msg.arg1);
+ return true;
+ }
+
+ if (!curMethod.startStylusHandwriting(session.getRequestId(),
+ session.getHandwritingChannel(), session.getRecordedEvents())) {
+ // When failed to issue IPCs, re-initialize handwriting state.
+ Slog.w(TAG, "Resetting handwriting mode.");
+ scheduleResetStylusHandwriting();
+ }
+ }
+ return true;
+ case MSG_FINISH_HANDWRITING:
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null && mHwController.getCurrentRequestId().isPresent()) {
+ curMethod.finishStylusHandwriting();
+ }
+ }
+ return true;
+ case MSG_REMOVE_HANDWRITING_WINDOW:
+ synchronized (ImfLock.class) {
+ IInputMethodInvoker curMethod = getCurMethodLocked();
+ if (curMethod != null) {
+ curMethod.removeStylusHandwritingWindow();
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @BinderThread
+ private void onStylusHandwritingReady(int requestId, int pid) {
+ mHandler.obtainMessage(MSG_START_HANDWRITING, requestId, pid).sendToTarget();
+ }
+
+ private void handleSetInteractive(final boolean interactive) {
+ synchronized (ImfLock.class) {
+ mIsInteractive = interactive;
+ updateSystemUiLocked(interactive ? mImeWindowVis : 0, mBackDisposition);
+
+ // Inform the current client of the change in active status
+ if (mCurClient == null || mCurClient.mClient == null) {
+ return;
+ }
+ if (mImePlatformCompatUtils.shouldUseSetInteractiveProtocol(getCurMethodUidLocked())) {
+ // Handle IME visibility when interactive changed before finishing the input to
+ // ensure we preserve the last state as possible.
+ final ImeVisibilityResult imeVisRes = mVisibilityStateComputer.onInteractiveChanged(
+ mCurFocusedWindow, interactive);
+ if (imeVisRes != null) {
+ mVisibilityApplier.applyImeVisibility(mCurFocusedWindow, null,
+ imeVisRes.getState(), imeVisRes.getReason());
+ }
+ // Eligible IME processes use new "setInteractive" protocol.
+ mCurClient.mClient.setInteractive(mIsInteractive, mInFullscreenMode);
+ } else {
+ // Legacy IME processes continue using legacy "setActive" protocol.
+ mCurClient.mClient.setActive(mIsInteractive, mInFullscreenMode);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean chooseNewDefaultIMELocked() {
+ final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME(
+ mSettings.getEnabledInputMethodListLocked());
+ if (imi != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "New default IME was selected: " + imi.getId());
+ }
+ resetSelectedInputMethodAndSubtypeLocked(imi.getId());
+ return true;
+ }
+
+ return false;
+ }
+
+ static void queryInputMethodServicesInternal(Context context,
+ @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
+ @DirectBootAwareness int directBootAwareness, List<String> enabledInputMethodList) {
+ final Context userAwareContext = context.getUserId() == userId
+ ? context
+ : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
+
+ methodList.clear();
+ methodMap.clear();
+
+ final int directBootAwarenessFlags;
+ switch (directBootAwareness) {
+ case DirectBootAwareness.ANY:
+ directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ break;
+ case DirectBootAwareness.AUTO:
+ directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AUTO;
+ break;
+ default:
+ directBootAwarenessFlags = PackageManager.MATCH_DIRECT_BOOT_AUTO;
+ Slog.e(TAG, "Unknown directBootAwareness=" + directBootAwareness
+ + ". Falling back to DirectBootAwareness.AUTO");
+ break;
+ }
+ final int flags = PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ | directBootAwarenessFlags;
+ final List<ResolveInfo> services = userAwareContext.getPackageManager().queryIntentServices(
+ new Intent(InputMethod.SERVICE_INTERFACE),
+ PackageManager.ResolveInfoFlags.of(flags));
+
+ methodList.ensureCapacity(services.size());
+ methodMap.ensureCapacity(services.size());
+
+ filterInputMethodServices(additionalSubtypeMap, methodMap, methodList,
+ enabledInputMethodList, userAwareContext, services);
+ }
+
+ static void filterInputMethodServices(
+ ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
+ List<String> enabledInputMethodList, Context userAwareContext,
+ List<ResolveInfo> services) {
+ final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
+
+ for (int i = 0; i < services.size(); ++i) {
+ ResolveInfo ri = services.get(i);
+ ServiceInfo si = ri.serviceInfo;
+ final String imeId = InputMethodInfo.computeId(ri);
+ if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {
+ Slog.w(TAG, "Skipping input method " + imeId
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_INPUT_METHOD);
+ continue;
+ }
+
+ if (DEBUG) Slog.d(TAG, "Checking " + imeId);
+
+ try {
+ final InputMethodInfo imi = new InputMethodInfo(userAwareContext, ri,
+ additionalSubtypeMap.get(imeId));
+ if (imi.isVrOnly()) {
+ continue; // Skip VR-only IME, which isn't supported for now.
+ }
+ final String packageName = si.packageName;
+ // only include IMEs which are from the system, enabled, or below the threshold
+ if (si.applicationInfo.isSystemApp() || enabledInputMethodList.contains(imi.getId())
+ || imiPackageCount.getOrDefault(packageName, 0)
+ < InputMethodInfo.MAX_IMES_PER_PACKAGE) {
+ imiPackageCount.put(packageName,
+ 1 + imiPackageCount.getOrDefault(packageName, 0));
+
+ methodList.add(imi);
+ methodMap.put(imi.getId(), imi);
+ if (DEBUG) {
+ Slog.d(TAG, "Found an input method " + imi);
+ }
+ } else if (DEBUG) {
+ Slog.d(TAG, "Found an input method, but ignored due threshold: " + imi);
+ }
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Unable to load input method " + imeId, e);
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void buildInputMethodListLocked(boolean resetDefaultEnabledIme) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme
+ + " \n ------ caller=" + Debug.getCallers(10));
+ }
+ if (!mSystemReady) {
+ Slog.e(TAG, "buildInputMethodListLocked is not allowed until system is ready");
+ return;
+ }
+ mMethodMapUpdateCount++;
+ mMyPackageMonitor.clearKnownImePackageNamesLocked();
+
+ queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(),
+ mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
+
+ // Construct the set of possible IME packages for onPackageChanged() to avoid false
+ // negatives when the package state remains to be the same but only the component state is
+ // changed.
+ {
+ // Here we intentionally use PackageManager.MATCH_DISABLED_COMPONENTS since the purpose
+ // of this query is to avoid false negatives. PackageManager.MATCH_ALL could be more
+ // conservative, but it seems we cannot use it for now (Issue 35176630).
+ final List<ResolveInfo> allInputMethodServices =
+ mContext.getPackageManager().queryIntentServicesAsUser(
+ new Intent(InputMethod.SERVICE_INTERFACE),
+ PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getCurrentUserId());
+ final int numImes = allInputMethodServices.size();
+ for (int i = 0; i < numImes; ++i) {
+ final ServiceInfo si = allInputMethodServices.get(i).serviceInfo;
+ if (android.Manifest.permission.BIND_INPUT_METHOD.equals(si.permission)) {
+ mMyPackageMonitor.addKnownImePackageNameLocked(si.packageName);
+ }
+ }
+ }
+
+ boolean reenableMinimumNonAuxSystemImes = false;
+ // TODO: The following code should find better place to live.
+ if (!resetDefaultEnabledIme) {
+ boolean enabledImeFound = false;
+ boolean enabledNonAuxImeFound = false;
+ final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked();
+ final int numImes = enabledImes.size();
+ for (int i = 0; i < numImes; ++i) {
+ final InputMethodInfo imi = enabledImes.get(i);
+ if (mMethodList.contains(imi)) {
+ enabledImeFound = true;
+ if (!imi.isAuxiliaryIme()) {
+ enabledNonAuxImeFound = true;
+ break;
+ }
+ }
+ }
+ if (!enabledImeFound) {
+ if (DEBUG) {
+ Slog.i(TAG, "All the enabled IMEs are gone. Reset default enabled IMEs.");
+ }
+ resetDefaultEnabledIme = true;
+ resetSelectedInputMethodAndSubtypeLocked("");
+ } else if (!enabledNonAuxImeFound) {
+ if (DEBUG) {
+ Slog.i(TAG, "All the enabled non-Aux IMEs are gone. Do partial reset.");
+ }
+ reenableMinimumNonAuxSystemImes = true;
+ }
+ }
+
+ if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) {
+ final ArrayList<InputMethodInfo> defaultEnabledIme =
+ InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList,
+ reenableMinimumNonAuxSystemImes);
+ final int numImes = defaultEnabledIme.size();
+ for (int i = 0; i < numImes; ++i) {
+ final InputMethodInfo imi = defaultEnabledIme.get(i);
+ if (DEBUG) {
+ Slog.d(TAG, "--- enable ime = " + imi);
+ }
+ setInputMethodEnabledLocked(imi.getId(), true);
+ }
+ }
+
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+ if (!TextUtils.isEmpty(defaultImiId)) {
+ if (!mMethodMap.containsKey(defaultImiId)) {
+ Slog.w(TAG, "Default IME is uninstalled. Choose new default IME.");
+ if (chooseNewDefaultIMELocked()) {
+ updateInputMethodsFromSettingsLocked(true);
+ }
+ } else {
+ // Double check that the default IME is certainly enabled.
+ setInputMethodEnabledLocked(defaultImiId, true);
+ }
+ }
+
+ updateDefaultVoiceImeIfNeededLocked();
+
+ // Here is not the perfect place to reset the switching controller. Ideally
+ // mSwitchingController and mSettings should be able to share the same state.
+ // TODO: Make sure that mSwitchingController and mSettings are sharing the
+ // the same enabled IMEs list.
+ mSwitchingController.resetCircularListLocked(mContext);
+ mHardwareKeyboardShortcutController.reset(mSettings);
+
+ sendOnNavButtonFlagsChangedLocked();
+
+ // Notify InputMethodListListeners of the new installed InputMethods.
+ final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList);
+ mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED,
+ mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget();
+ }
+
+ @GuardedBy("ImfLock.class")
+ void sendOnNavButtonFlagsChangedLocked() {
+ final IInputMethodInvoker curMethod = mBindingController.getCurMethod();
+ if (curMethod == null) {
+ // No need to send the data if the IME is not yet bound.
+ return;
+ }
+ curMethod.onNavButtonFlagsChanged(getInputMethodNavButtonFlagsLocked());
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void updateDefaultVoiceImeIfNeededLocked() {
+ final String systemSpeechRecognizer =
+ mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer);
+ final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod();
+ final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme(
+ mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId);
+ if (newSystemVoiceIme == null) {
+ if (DEBUG) {
+ Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked,"
+ + " this may be expected.");
+ }
+ // Clear DEFAULT_VOICE_INPUT_METHOD when necessary. Note that InputMethodSettings
+ // does not update the actual Secure Settings until the user is unlocked.
+ if (!TextUtils.isEmpty(currentDefaultVoiceImeId)) {
+ mSettings.putDefaultVoiceInputMethod("");
+ // We don't support disabling the voice ime when a package is removed from the
+ // config.
+ }
+ return;
+ }
+ if (TextUtils.equals(currentDefaultVoiceImeId, newSystemVoiceIme.getId())) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "Enabling the default Voice IME:" + newSystemVoiceIme);
+ }
+ setInputMethodEnabledLocked(newSystemVoiceIme.getId(), true);
+ mSettings.putDefaultVoiceInputMethod(newSystemVoiceIme.getId());
+ }
+
+ // ----------------------------------------------------------------------
+
+ /**
+ * Enable or disable the given IME by updating {@link Settings.Secure#ENABLED_INPUT_METHODS}.
+ *
+ * @param id ID of the IME is to be manipulated. It is OK to pass IME ID that is currently not
+ * recognized by the system.
+ * @param enabled {@code true} if {@code id} needs to be enabled.
+ * @return {@code true} if the IME was previously enabled. {@code false} otherwise.
+ */
+ @GuardedBy("ImfLock.class")
+ private boolean setInputMethodEnabledLocked(String id, boolean enabled) {
+ List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings
+ .getEnabledInputMethodsAndSubtypeListLocked();
+
+ if (enabled) {
+ for (Pair<String, ArrayList<String>> pair: enabledInputMethodsList) {
+ if (pair.first.equals(id)) {
+ // We are enabling this input method, but it is already enabled.
+ // Nothing to do. The previous state was enabled.
+ return true;
+ }
+ }
+ mSettings.appendAndPutEnabledInputMethodLocked(id, false);
+ // Previous state was disabled.
+ return false;
+ } else {
+ StringBuilder builder = new StringBuilder();
+ if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ builder, enabledInputMethodsList, id)) {
+ // Disabled input method is currently selected, switch to another one.
+ final String selId = mSettings.getSelectedInputMethod();
+ if (id.equals(selId) && !chooseNewDefaultIMELocked()) {
+ Slog.i(TAG, "Can't find new IME, unsetting the current input method.");
+ resetSelectedInputMethodAndSubtypeLocked("");
+ }
+ // Previous state was enabled.
+ return true;
+ } else {
+ // We are disabling the input method but it is already disabled.
+ // Nothing to do. The previous state was disabled.
+ return false;
+ }
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId,
+ boolean setSubtypeOnly) {
+ mSettings.saveCurrentInputMethodAndSubtypeToHistory(getSelectedMethodIdLocked(),
+ mCurrentSubtype);
+
+ // Set Subtype here
+ if (imi == null || subtypeId < 0) {
+ mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ mCurrentSubtype = null;
+ } else {
+ if (subtypeId < imi.getSubtypeCount()) {
+ InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId);
+ mSettings.putSelectedSubtype(subtype.hashCode());
+ mCurrentSubtype = subtype;
+ } else {
+ mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ // If the subtype is not specified, choose the most applicable one
+ mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
+ }
+ }
+ notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype);
+
+ if (!setSubtypeOnly) {
+ // Set InputMethod here
+ mSettings.putSelectedInputMethod(imi != null ? imi.getId() : "");
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
+ InputMethodInfo imi = mMethodMap.get(newDefaultIme);
+ int lastSubtypeId = NOT_A_SUBTYPE_ID;
+ // newDefaultIme is empty when there is no candidate for the selected IME.
+ if (imi != null && !TextUtils.isEmpty(newDefaultIme)) {
+ String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme);
+ if (subtypeHashCode != null) {
+ try {
+ lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi,
+ Integer.parseInt(subtypeHashCode));
+ } catch (NumberFormatException e) {
+ Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e);
+ }
+ }
+ }
+ setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false);
+ }
+
+ /**
+ * Gets the current subtype of this input method.
+ *
+ * @param userId User ID to be queried about.
+ * @return The current {@link InputMethodSubtype} for the specified user.
+ */
+ @Nullable
+ @Override
+ public InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
+ if (UserHandle.getCallingUserId() != userId) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
+ }
+ synchronized (ImfLock.class) {
+ if (mSettings.getCurrentUserId() == userId) {
+ return getCurrentInputMethodSubtypeLocked();
+ }
+
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
+ return settings.getCurrentInputMethodSubtypeForNonCurrentUsers();
+ }
+ }
+
+ /**
+ * Returns the current {@link InputMethodSubtype} for the current user.
+ *
+ * <p>CAVEATS: You must also update
+ * {@link InputMethodSettings#getCurrentInputMethodSubtypeForNonCurrentUsers()}
+ * when you update the algorithm of this method.</p>
+ *
+ * <p>TODO: Address code duplication between this and
+ * {@link InputMethodSettings#getCurrentInputMethodSubtypeForNonCurrentUsers()}.</p>
+ */
+ @GuardedBy("ImfLock.class")
+ InputMethodSubtype getCurrentInputMethodSubtypeLocked() {
+ String selectedMethodId = getSelectedMethodIdLocked();
+ if (selectedMethodId == null) {
+ return null;
+ }
+ final boolean subtypeIsSelected = mSettings.isSubtypeSelected();
+ final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+ if (imi == null || imi.getSubtypeCount() == 0) {
+ return null;
+ }
+ if (!subtypeIsSelected || mCurrentSubtype == null
+ || !SubtypeUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) {
+ int subtypeId = mSettings.getSelectedInputMethodSubtypeId(selectedMethodId);
+ if (subtypeId == NOT_A_SUBTYPE_ID) {
+ // If there are no selected subtypes, the framework will try to find
+ // the most applicable subtype from explicitly or implicitly enabled
+ // subtypes.
+ List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
+ mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ // If there is only one explicitly or implicitly enabled subtype,
+ // just returns it.
+ if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
+ mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0);
+ } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) {
+ mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ mRes, explicitlyOrImplicitlyEnabledSubtypes,
+ SubtypeUtils.SUBTYPE_MODE_KEYBOARD, null, true);
+ if (mCurrentSubtype == null) {
+ mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null, true);
+ }
+ }
+ } else {
+ mCurrentSubtype = SubtypeUtils.getSubtypes(imi).get(subtypeId);
+ }
+ }
+ return mCurrentSubtype;
+ }
+
+ /**
+ * Returns the default {@link InputMethodInfo} for the specific userId.
+ * @param userId user ID to query.
+ */
+ @GuardedBy("ImfLock.class")
+ private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) {
+ final String imeId = mSettings.getSelectedInputMethodForUser(userId);
+ if (TextUtils.isEmpty(imeId)) {
+ Slog.e(TAG, "No default input method found for userId " + userId);
+ return null;
+ }
+
+ InputMethodInfo curInputMethodInfo;
+ if (userId == mSettings.getCurrentUserId()
+ && (curInputMethodInfo = mMethodMap.get(imeId)) != null) {
+ // clone the InputMethodInfo before returning.
+ return new InputMethodInfo(curInputMethodInfo);
+ }
+
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ Context userAwareContext =
+ mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
+
+ final int flags = PackageManager.GET_META_DATA
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_AUTO;
+ final List<ResolveInfo> services =
+ userAwareContext.getPackageManager().queryIntentServicesAsUser(
+ new Intent(InputMethod.SERVICE_INTERFACE),
+ PackageManager.ResolveInfoFlags.of(flags),
+ userId);
+ for (ResolveInfo ri : services) {
+ final String imeIdResolved = InputMethodInfo.computeId(ri);
+ if (imeId.equals(imeIdResolved)) {
+ try {
+ return new InputMethodInfo(
+ userAwareContext, ri, additionalSubtypeMap.get(imeId));
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Unable to load input method " + imeId, e);
+ }
+ }
+ }
+ // we didn't find the InputMethodInfo for imeId. This shouldn't happen.
+ Slog.e(TAG, "Error while locating input method info for imeId: " + imeId);
+ return null;
+ }
+ private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) {
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+ new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+ methodMap, methodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
+ return methodMap;
+ }
+
+ @GuardedBy("ImfLock.class")
+ private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
+ if (userId == mSettings.getCurrentUserId()) {
+ if (!mMethodMap.containsKey(imeId)
+ || !mSettings.getEnabledInputMethodListLocked()
+ .contains(mMethodMap.get(imeId))) {
+ return false; // IME is not found or not enabled.
+ }
+ setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID);
+ return true;
+ }
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap, userId,
+ false);
+ if (!methodMap.containsKey(imeId)
+ || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) {
+ return false; // IME is not found or not enabled.
+ }
+ settings.putSelectedInputMethod(imeId);
+ settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ return true;
+ }
+
+ /**
+ * Filter the access to the input method by rules of the package visibility. Return {@code true}
+ * if the given input method is the currently selected one or visible to the caller.
+ *
+ * @param targetPkgName The package name of input method to check.
+ * @param callingUid The caller that is going to access the input method.
+ * @param userId The user ID where the input method resides.
+ * @param settings The input method settings under the given user ID.
+ * @return {@code true} if caller is able to access the input method.
+ */
+ private boolean canCallerAccessInputMethod(@NonNull String targetPkgName, int callingUid,
+ @UserIdInt int userId, @NonNull InputMethodSettings settings) {
+ final String methodId = settings.getSelectedInputMethod();
+ final ComponentName selectedInputMethod = methodId != null
+ ? InputMethodUtils.convertIdToComponentName(methodId) : null;
+ if (selectedInputMethod != null
+ && selectedInputMethod.getPackageName().equals(targetPkgName)) {
+ return true;
+ }
+ final boolean canAccess = !mPackageManagerInternal.filterAppAccess(
+ targetPkgName, callingUid, userId);
+ if (DEBUG && !canAccess) {
+ Slog.d(TAG, "Input method " + targetPkgName
+ + " is not visible to the caller " + callingUid);
+ }
+ return canAccess;
+ }
+
+ // Start CarInputMethodManagerService
+ private void publishLocalService() {
+ LocalServices.addService(InputMethodManagerInternal.class, new LocalServiceImpl());
+ }
+
+ void notifySystemUnlockUser(@UserIdInt int userId) {
+ mHandler.obtainMessage(MSG_SYSTEM_UNLOCK_USER, userId, 0).sendToTarget();
+ }
+ // End CarInputMethodManagerService
+
+ private final class LocalServiceImpl extends InputMethodManagerInternal {
+
+ @Override
+ public void setInteractive(boolean interactive) {
+ // Do everything in handler so as not to block the caller.
+ mHandler.obtainMessage(MSG_SET_INTERACTIVE, interactive ? 1 : 0, 0).sendToTarget();
+ }
+
+ @Override
+ public void hideCurrentInputMethod(@SoftInputShowHideReason int reason) {
+ mHandler.removeMessages(MSG_HIDE_CURRENT_INPUT_METHOD);
+ mHandler.obtainMessage(MSG_HIDE_CURRENT_INPUT_METHOD, reason).sendToTarget();
+ }
+
+ @Override
+ public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ return getInputMethodListLocked(userId, DirectBootAwareness.AUTO,
+ Process.SYSTEM_UID);
+ }
+ }
+
+ @Override
+ public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ return getEnabledInputMethodListLocked(userId, Process.SYSTEM_UID);
+ }
+ }
+
+ @Override
+ public void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
+ InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback cb) {
+ // Get the device global touch exploration state before lock to avoid deadlock.
+ final boolean touchExplorationEnabled = AccessibilityManagerInternal.get()
+ .isTouchExplorationEnabled(userId);
+
+ synchronized (ImfLock.class) {
+ mAutofillController.onCreateInlineSuggestionsRequest(userId, requestInfo, cb,
+ touchExplorationEnabled);
+ }
+ }
+
+ @Override
+ public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ return switchToInputMethodLocked(imeId, userId);
+ }
+ }
+
+ @Override
+ public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) {
+ synchronized (ImfLock.class) {
+ if (userId == mSettings.getCurrentUserId()) {
+ if (!mMethodMap.containsKey(imeId)) {
+ return false; // IME is not found.
+ }
+ setInputMethodEnabledLocked(imeId, enabled);
+ return true;
+ }
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
+ if (!methodMap.containsKey(imeId)) {
+ return false; // IME is not found.
+ }
+ if (enabled) {
+ if (!settings.getEnabledInputMethodListLocked().contains(
+ methodMap.get(imeId))) {
+ settings.appendAndPutEnabledInputMethodLocked(imeId, false);
+ }
+ } else {
+ settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ new StringBuilder(),
+ settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+ }
+ return true;
+ }
+ }
+
+ @Override
+ public void registerInputMethodListListener(InputMethodListListener listener) {
+ mInputMethodListListeners.addIfAbsent(listener);
+ }
+
+ @Override
+ public boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken,
+ int displayId) {
+ //TODO(b/150843766): Check if Input Token is valid.
+ final IBinder curHostInputToken;
+ synchronized (ImfLock.class) {
+ if (displayId != mCurTokenDisplayId || mCurHostInputToken == null) {
+ return false;
+ }
+ curHostInputToken = mCurHostInputToken;
+ }
+ return mInputManagerInternal.transferTouchFocus(sourceInputToken, curHostInputToken);
+ }
+
+ @Override
+ public void reportImeControl(@Nullable IBinder windowToken) {
+ synchronized (ImfLock.class) {
+ if (mCurFocusedWindow != windowToken) {
+ // mCurPerceptible was set by the focused window, but it is no longer in
+ // control, so we reset mCurPerceptible.
+ mCurPerceptible = true;
+ }
+ }
+ }
+
+ @Override
+ public void onImeParentChanged() {
+ synchronized (ImfLock.class) {
+ // Hide the IME method menu only when the IME surface parent is changed by the
+ // input target changed, in case seeing the dialog dismiss flickering during
+ // the next focused window starting the input connection.
+ if (mLastImeTargetWindow != mCurFocusedWindow) {
+ mMenuController.hideInputMethodMenu();
+ }
+ }
+ }
+
+ @Override
+ public void removeImeSurface() {
+ mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
+ }
+
+ @Override
+ public void updateImeWindowStatus(boolean disableImeIcon) {
+ mHandler.obtainMessage(MSG_UPDATE_IME_WINDOW_STATUS, disableImeIcon ? 1 : 0, 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
+ IAccessibilityInputMethodSession session) {
+ synchronized (ImfLock.class) {
+ if (mCurClient != null) {
+ clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId);
+ mCurClient.mAccessibilitySessions.put(accessibilityConnectionId,
+ new AccessibilitySessionState(mCurClient, accessibilityConnectionId,
+ session));
+
+ attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY,
+ true);
+
+ final SessionState sessionState = mCurClient.mCurSession;
+ final IInputMethodSession imeSession = sessionState == null
+ ? null : sessionState.mSession;
+ final SparseArray<IAccessibilityInputMethodSession>
+ accessibilityInputMethodSessions =
+ createAccessibilityInputMethodSessions(
+ mCurClient.mAccessibilitySessions);
+ final InputBindResult res = new InputBindResult(
+ InputBindResult.ResultCode.SUCCESS_WITH_ACCESSIBILITY_SESSION,
+ imeSession, accessibilityInputMethodSessions, null, getCurIdLocked(),
+ getSequenceNumberLocked(), mCurVirtualDisplayToScreenMatrix, false);
+ mCurClient.mClient.onBindAccessibilityService(res, accessibilityConnectionId);
+ }
+ }
+ }
+
+ @Override
+ public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId) {
+ synchronized (ImfLock.class) {
+ if (mCurClient != null) {
+ if (DEBUG) {
+ Slog.v(TAG, "unbindAccessibilityFromCurrentClientLocked: client="
+ + mCurClient.mClient.asBinder());
+ }
+ // A11yManagerService unbinds the disabled accessibility service. We don't need
+ // to do it here.
+ mCurClient.mClient.onUnbindAccessibilityService(getSequenceNumberLocked(),
+ accessibilityConnectionId);
+ }
+ // We only have sessions when we bound to an input method. Remove this session
+ // from all clients.
+ if (getCurMethodLocked() != null) {
+ final int numClients = mClients.size();
+ for (int i = 0; i < numClients; ++i) {
+ clearClientSessionForAccessibilityLocked(mClients.valueAt(i),
+ accessibilityConnectionId);
+ }
+ AccessibilitySessionState session = mEnabledAccessibilitySessions.get(
+ accessibilityConnectionId);
+ if (session != null) {
+ finishSessionForAccessibilityLocked(session);
+ mEnabledAccessibilitySessions.remove(accessibilityConnectionId);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void maybeFinishStylusHandwriting() {
+ mHandler.removeMessages(MSG_FINISH_HANDWRITING);
+ mHandler.obtainMessage(MSG_FINISH_HANDWRITING).sendToTarget();
+ }
+
+ @Override
+ public void switchKeyboardLayout(int direction) {
+ synchronized (ImfLock.class) {
+ final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked());
+ if (currentImi == null) {
+ return;
+ }
+ final InputMethodSubtypeHandle currentSubtypeHandle =
+ InputMethodSubtypeHandle.of(currentImi, mCurrentSubtype);
+ final InputMethodSubtypeHandle nextSubtypeHandle =
+ mHardwareKeyboardShortcutController.onSubtypeSwitch(currentSubtypeHandle,
+ direction > 0);
+ if (nextSubtypeHandle == null) {
+ return;
+ }
+ final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId());
+ if (nextImi == null) {
+ return;
+ }
+
+ final int subtypeCount = nextImi.getSubtypeCount();
+ if (subtypeCount == 0) {
+ if (nextSubtypeHandle.equals(InputMethodSubtypeHandle.of(nextImi, null))) {
+ setInputMethodLocked(nextImi.getId(), NOT_A_SUBTYPE_ID);
+ }
+ return;
+ }
+
+ for (int i = 0; i < subtypeCount; ++i) {
+ if (nextSubtypeHandle.equals(
+ InputMethodSubtypeHandle.of(nextImi, nextImi.getSubtypeAt(i)))) {
+ setInputMethodLocked(nextImi.getId(), i);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ @BinderThread
+ private IInputContentUriToken createInputContentUriToken(@Nullable IBinder token,
+ @Nullable Uri contentUri, @Nullable String packageName) {
+ if (token == null) {
+ throw new NullPointerException("token");
+ }
+ if (packageName == null) {
+ throw new NullPointerException("packageName");
+ }
+ if (contentUri == null) {
+ throw new NullPointerException("contentUri");
+ }
+ final String contentUriScheme = contentUri.getScheme();
+ if (!"content".equals(contentUriScheme)) {
+ throw new InvalidParameterException("contentUri must have content scheme");
+ }
+
+ synchronized (ImfLock.class) {
+ final int uid = Binder.getCallingUid();
+ if (getSelectedMethodIdLocked() == null) {
+ return null;
+ }
+ if (getCurTokenLocked() != token) {
+ Slog.e(TAG, "Ignoring createInputContentUriToken mCurToken=" + getCurTokenLocked()
+ + " token=" + token);
+ return null;
+ }
+ // We cannot simply distinguish a bad IME that reports an arbitrary package name from
+ // an unfortunate IME whose internal state is already obsolete due to the asynchronous
+ // nature of our system. Let's compare it with our internal record.
+ final String curPackageName = mCurEditorInfo != null
+ ? mCurEditorInfo.packageName : null;
+ if (!TextUtils.equals(curPackageName, packageName)) {
+ Slog.e(TAG, "Ignoring createInputContentUriToken mCurEditorInfo.packageName="
+ + curPackageName + " packageName=" + packageName);
+ return null;
+ }
+ // This user ID can never bee spoofed.
+ final int imeUserId = UserHandle.getUserId(uid);
+ // This user ID can never bee spoofed.
+ final int appUserId = UserHandle.getUserId(mCurClient.mUid);
+ // This user ID may be invalid if "contentUri" embedded an invalid user ID.
+ final int contentUriOwnerUserId = ContentProvider.getUserIdFromUri(contentUri,
+ imeUserId);
+ final Uri contentUriWithoutUserId = ContentProvider.getUriWithoutUserId(contentUri);
+ // Note: InputContentUriTokenHandler.take() checks whether the IME (specified by "uid")
+ // actually has the right to grant a read permission for "contentUriWithoutUserId" that
+ // is claimed to belong to "contentUriOwnerUserId". For example, specifying random
+ // content URI and/or contentUriOwnerUserId just results in a SecurityException thrown
+ // from InputContentUriTokenHandler.take() and can never be allowed beyond what is
+ // actually allowed to "uid", which is guaranteed to be the IME's one.
+ return new InputContentUriTokenHandler(contentUriWithoutUserId, uid,
+ packageName, contentUriOwnerUserId, appUserId);
+ }
+ }
+
+ @BinderThread
+ private void reportFullscreenMode(@NonNull IBinder token, boolean fullscreen) {
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token)) {
+ return;
+ }
+ if (mCurClient != null && mCurClient.mClient != null) {
+ mInFullscreenMode = fullscreen;
+ mCurClient.mClient.reportFullscreenMode(fullscreen);
+ }
+ }
+ }
+
+ private final PriorityDump.PriorityDumper mPriorityDumper = new PriorityDump.PriorityDumper() {
+ /**
+ * {@inheritDoc}
+ */
+ @BinderThread
+ @Override
+ public void dumpCritical(FileDescriptor fd, PrintWriter pw, String[] args,
+ boolean asProto) {
+ if (asProto) {
+ dumpAsProtoNoCheck(fd);
+ } else {
+ dumpAsStringNoCheck(fd, pw, args, true /* isCritical */);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @BinderThread
+ @Override
+ public void dumpHigh(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
+ dumpNormal(fd, pw, args, asProto);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @BinderThread
+ @Override
+ public void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
+ if (asProto) {
+ dumpAsProtoNoCheck(fd);
+ } else {
+ dumpAsStringNoCheck(fd, pw, args, false /* isCritical */);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @BinderThread
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
+ dumpNormal(fd, pw, args, asProto);
+ }
+
+ @BinderThread
+ private void dumpAsProtoNoCheck(FileDescriptor fd) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+ dumpDebug(proto, InputMethodManagerServiceTraceProto.INPUT_METHOD_MANAGER_SERVICE);
+ proto.flush();
+ }
+ };
+
+ @BinderThread
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+
+ PriorityDump.dump(mPriorityDumper, fd, pw, args);
+ }
+
+ @BinderThread
+ private void dumpAsStringNoCheck(FileDescriptor fd, PrintWriter pw, String[] args,
+ boolean isCritical) {
+ IInputMethodInvoker method;
+ ClientState client;
+ ClientState focusedWindowClient;
+
+ final Printer p = new PrintWriterPrinter(pw);
+
+ synchronized (ImfLock.class) {
+ p.println("Current Input Method Manager state:");
+ int numImes = mMethodList.size();
+ p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount);
+ for (int i = 0; i < numImes; i++) {
+ InputMethodInfo info = mMethodList.get(i);
+ p.println(" InputMethod #" + i + ":");
+ info.dump(p, " ");
+ }
+ p.println(" Clients:");
+ final int numClients = mClients.size();
+ for (int i = 0; i < numClients; ++i) {
+ final ClientState ci = mClients.valueAt(i);
+ p.println(" Client " + ci + ":");
+ p.println(" client=" + ci.mClient);
+ p.println(" fallbackInputConnection=" + ci.mFallbackInputConnection);
+ p.println(" sessionRequested=" + ci.mSessionRequested);
+ p.println(" sessionRequestedForAccessibility="
+ + ci.mSessionRequestedForAccessibility);
+ p.println(" curSession=" + ci.mCurSession);
+ }
+ p.println(" mCurMethodId=" + getSelectedMethodIdLocked());
+ client = mCurClient;
+ p.println(" mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
+ p.println(" mCurPerceptible=" + mCurPerceptible);
+ p.println(" mCurFocusedWindow=" + mCurFocusedWindow
+ + " softInputMode="
+ + InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)
+ + " client=" + mCurFocusedWindowClient);
+ focusedWindowClient = mCurFocusedWindowClient;
+ p.println(" mCurId=" + getCurIdLocked() + " mHaveConnection=" + hasConnectionLocked()
+ + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
+ + mBindingController.isVisibleBound());
+ p.println(" mCurToken=" + getCurTokenLocked());
+ p.println(" mCurTokenDisplayId=" + mCurTokenDisplayId);
+ p.println(" mCurHostInputToken=" + mCurHostInputToken);
+ p.println(" mCurIntent=" + getCurIntentLocked());
+ method = getCurMethodLocked();
+ p.println(" mCurMethod=" + getCurMethodLocked());
+ p.println(" mEnabledSession=" + mEnabledSession);
+ mVisibilityStateComputer.dump(pw);
+ p.println(" mInFullscreenMode=" + mInFullscreenMode);
+ p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive);
+ p.println(" mSettingsObserver=" + mSettingsObserver);
+ p.println(" mStylusIds=" + (mStylusIds != null
+ ? Arrays.toString(mStylusIds.toArray()) : ""));
+ p.println(" mSwitchingController:");
+ mSwitchingController.dump(p);
+ p.println(" mSettings:");
+ mSettings.dumpLocked(p, " ");
+
+ p.println(" mStartInputHistory:");
+ mStartInputHistory.dump(pw, " ");
+
+ p.println(" mSoftInputShowHideHistory:");
+ mSoftInputShowHideHistory.dump(pw, " ");
+
+ p.println(" mImeTrackerService#History:");
+ mImeTrackerService.dump(pw, " ");
+ }
+
+ // Exit here for critical dump, as remaining sections require IPCs to other processes.
+ if (isCritical) {
+ return;
+ }
+
+ p.println(" ");
+ if (client != null) {
+ pw.flush();
+ try {
+ TransferPipe.dumpAsync(client.mClient.asBinder(), fd, args);
+ } catch (IOException | RemoteException e) {
+ p.println("Failed to dump input method client: " + e);
+ }
+ } else {
+ p.println("No input method client.");
+ }
+
+ if (focusedWindowClient != null && client != focusedWindowClient) {
+ p.println(" ");
+ p.println("Warning: Current input method client doesn't match the last focused. "
+ + "window.");
+ p.println("Dumping input method client in the last focused window just in case.");
+ p.println(" ");
+ pw.flush();
+ try {
+ TransferPipe.dumpAsync(focusedWindowClient.mClient.asBinder(), fd, args);
+ } catch (IOException | RemoteException e) {
+ p.println("Failed to dump input method client in focused window: " + e);
+ }
+ }
+
+ p.println(" ");
+ if (method != null) {
+ pw.flush();
+ try {
+ TransferPipe.dumpAsync(method.asBinder(), fd, args);
+ } catch (IOException | RemoteException e) {
+ p.println("Failed to dump input method service: " + e);
+ }
+ } else {
+ p.println("No input method service.");
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err,
+ @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ final int callingUid = Binder.getCallingUid();
+ // Reject any incoming calls from non-shell users, including ones from the system user.
+ if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) {
+ // Note that Binder#onTransact() will automatically close "in", "out", and "err" when
+ // returned from this method, hence there is no need to close those FDs.
+ // "resultReceiver" is the only thing that needs to be taken care of here.
+ if (resultReceiver != null) {
+ resultReceiver.send(ShellCommandResult.FAILURE, null);
+ }
+ final String errorMsg = "InputMethodManagerService does not support shell commands from"
+ + " non-shell users. callingUid=" + callingUid
+ + " args=" + Arrays.toString(args);
+ if (Process.isCoreUid(callingUid)) {
+ // Let's not crash the calling process if the caller is one of core components.
+ Slog.e(TAG, errorMsg);
+ return;
+ }
+ throw new SecurityException(errorMsg);
+ }
+ new ShellCommandImpl(this).exec(
+ this, in, out, err, args, callback, resultReceiver);
+ }
+
+ private static final class ShellCommandImpl extends ShellCommand {
+ // Start CarInputMethodManagerService
+ @NonNull
+ final CarInputMethodManagerService mService;
+
+ ShellCommandImpl(CarInputMethodManagerService service) {
+ mService = service;
+ }
+ // End CarInputMethodManagerService
+
+ @BinderThread
+ @ShellCommandResult
+ @Override
+ public int onCommand(@Nullable String cmd) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return onCommandWithSystemIdentity(cmd);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @BinderThread
+ @ShellCommandResult
+ private int onCommandWithSystemIdentity(@Nullable String cmd) {
+ switch (TextUtils.emptyIfNull(cmd)) {
+ case "get-last-switch-user-id":
+ return mService.getLastSwitchUserId(this);
+ case "tracing":
+ return mService.handleShellCommandTraceInputMethod(this);
+ case "ime": { // For "adb shell ime <command>".
+ final String imeCommand = TextUtils.emptyIfNull(getNextArg());
+ switch (imeCommand) {
+ case "":
+ case "-h":
+ case "help":
+ return onImeCommandHelp();
+ case "list":
+ return mService.handleShellCommandListInputMethods(this);
+ case "enable":
+ return mService.handleShellCommandEnableDisableInputMethod(this, true);
+ case "disable":
+ return mService.handleShellCommandEnableDisableInputMethod(this, false);
+ case "set":
+ return mService.handleShellCommandSetInputMethod(this);
+ case "reset":
+ return mService.handleShellCommandResetInputMethod(this);
+ case "tracing": // TODO(b/180765389): Unsupport "adb shell ime tracing"
+ return mService.handleShellCommandTraceInputMethod(this);
+ default:
+ getOutPrintWriter().println("Unknown command: " + imeCommand);
+ return ShellCommandResult.FAILURE;
+ }
+ }
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void onHelp() {
+ try (PrintWriter pw = getOutPrintWriter()) {
+ pw.println("CarInputMethodManagerService commands:");
+ pw.println(" help");
+ pw.println(" Prints this help text.");
+ pw.println(" dump [options]");
+ pw.println(" Synonym of dumpsys.");
+ pw.println(" ime <command> [options]");
+ pw.println(" Manipulate IMEs. Run \"ime help\" for details.");
+ pw.println(" tracing <command>");
+ pw.println(" start: Start tracing.");
+ pw.println(" stop : Stop tracing.");
+ pw.println(" help : Show help.");
+ }
+ }
+
+ @BinderThread
+ @ShellCommandResult
+ private int onImeCommandHelp() {
+ try (IndentingPrintWriter pw =
+ new IndentingPrintWriter(getOutPrintWriter(), " ", 100)) {
+ pw.println("ime <command>:");
+ pw.increaseIndent();
+
+ pw.println("list [-a] [-s]");
+ pw.increaseIndent();
+ pw.println("prints all enabled input methods.");
+ pw.increaseIndent();
+ pw.println("-a: see all input methods");
+ pw.println("-s: only a single summary line of each");
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println("enable [--user <USER_ID>] <ID>");
+ pw.increaseIndent();
+ pw.println("allows the given input method ID to be used.");
+ pw.increaseIndent();
+ pw.print("--user <USER_ID>: Specify which user to enable.");
+ pw.println(" Assumes the current user if not specified.");
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println("disable [--user <USER_ID>] <ID>");
+ pw.increaseIndent();
+ pw.println("disallows the given input method ID to be used.");
+ pw.increaseIndent();
+ pw.print("--user <USER_ID>: Specify which user to disable.");
+ pw.println(" Assumes the current user if not specified.");
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println("set [--user <USER_ID>] <ID>");
+ pw.increaseIndent();
+ pw.println("switches to the given input method ID.");
+ pw.increaseIndent();
+ pw.print("--user <USER_ID>: Specify which user to enable.");
+ pw.println(" Assumes the current user if not specified.");
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println("reset [--user <USER_ID>]");
+ pw.increaseIndent();
+ pw.println("reset currently selected/enabled IMEs to the default ones as if "
+ + "the device is initially booted with the current locale.");
+ pw.increaseIndent();
+ pw.print("--user <USER_ID>: Specify which user to reset.");
+ pw.println(" Assumes the current user if not specified.");
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // Shell command handlers:
+
+ @BinderThread
+ @ShellCommandResult
+ private int getLastSwitchUserId(@NonNull ShellCommand shellCommand) {
+ synchronized (ImfLock.class) {
+ shellCommand.getOutPrintWriter().println(mLastSwitchUserId);
+ return ShellCommandResult.SUCCESS;
+ }
+ }
+
+ /**
+ * Handles {@code adb shell ime list}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandListInputMethods(@NonNull ShellCommand shellCommand) {
+ boolean all = false;
+ boolean brief = false;
+ int userIdToBeResolved = UserHandle.USER_CURRENT;
+ while (true) {
+ final String nextOption = shellCommand.getNextOption();
+ if (nextOption == null) {
+ break;
+ }
+ switch (nextOption) {
+ case "-a":
+ all = true;
+ break;
+ case "-s":
+ brief = true;
+ break;
+ case "-u":
+ case "--user":
+ userIdToBeResolved = UserHandle.parseUserArg(shellCommand.getNextArgRequired());
+ break;
+ }
+ }
+ synchronized (ImfLock.class) {
+ final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
+ mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ try (PrintWriter pr = shellCommand.getOutPrintWriter()) {
+ for (int userId : userIds) {
+ final List<InputMethodInfo> methods = all
+ ? getInputMethodListLocked(
+ userId, DirectBootAwareness.AUTO, Process.SHELL_UID)
+ : getEnabledInputMethodListLocked(userId, Process.SHELL_UID);
+ if (userIds.length > 1) {
+ pr.print("User #");
+ pr.print(userId);
+ pr.println(":");
+ }
+ for (InputMethodInfo info : methods) {
+ if (brief) {
+ pr.println(info.getId());
+ } else {
+ pr.print(info.getId());
+ pr.println(":");
+ info.dump(pr::println, " ");
+ }
+ }
+ }
+ }
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell ime enable} and {@code adb shell ime disable}.
+ *
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @param enabled {@code true} if the command was {@code adb shell ime enable}.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandEnableDisableInputMethod(
+ @NonNull ShellCommand shellCommand, boolean enabled) {
+ final int userIdToBeResolved = handleOptionsForCommandsThatOnlyHaveUserOption(shellCommand);
+ final String imeId = shellCommand.getNextArgRequired();
+ boolean hasFailed = false;
+ try (PrintWriter out = shellCommand.getOutPrintWriter();
+ PrintWriter error = shellCommand.getErrPrintWriter()) {
+ synchronized (ImfLock.class) {
+ final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
+ mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ for (int userId : userIds) {
+ if (!userHasDebugPriv(userId, shellCommand)) {
+ continue;
+ }
+ hasFailed |= !handleShellCommandEnableDisableInputMethodInternalLocked(
+ userId, imeId, enabled, out, error);
+ }
+ }
+ }
+ return hasFailed ? ShellCommandResult.FAILURE : ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * A special helper method for commands that only have {@code -u} and {@code --user} options.
+ *
+ * <p>You cannot use this helper method if the command has other options.</p>
+ *
+ * <p>CAVEAT: This method must be called only once before any other
+ * {@link ShellCommand#getNextArg()} and {@link ShellCommand#getNextArgRequired()} for the
+ * main arguments.</p>
+ *
+ * @param shellCommand {@link ShellCommand} from which options should be obtained.
+ * @return User ID to be resolved. {@link UserHandle#CURRENT} if not specified.
+ */
+ @BinderThread
+ @UserIdInt
+ private static int handleOptionsForCommandsThatOnlyHaveUserOption(ShellCommand shellCommand) {
+ while (true) {
+ final String nextOption = shellCommand.getNextOption();
+ if (nextOption == null) {
+ break;
+ }
+ switch (nextOption) {
+ case "-u":
+ case "--user":
+ return UserHandle.parseUserArg(shellCommand.getNextArgRequired());
+ }
+ }
+ return UserHandle.USER_CURRENT;
+ }
+
+ /**
+ * Handles core logic of {@code adb shell ime enable} and {@code adb shell ime disable}.
+ *
+ * @param userId user ID specified to the command. Pseudo user IDs are not supported.
+ * @param imeId IME ID specified to the command.
+ * @param enabled {@code true} for {@code adb shell ime enable}. {@code false} otherwise.
+ * @param out {@link PrintWriter} to output standard messages.
+ * @param error {@link PrintWriter} to output error messages.
+ * @return {@code false} if it fails to enable the IME. {@code false} otherwise.
+ */
+ @BinderThread
+ @GuardedBy("ImfLock.class")
+ private boolean handleShellCommandEnableDisableInputMethodInternalLocked(
+ @UserIdInt int userId, String imeId, boolean enabled, PrintWriter out,
+ PrintWriter error) {
+ boolean failedToEnableUnknownIme = false;
+ boolean previouslyEnabled = false;
+ if (userId == mSettings.getCurrentUserId()) {
+ if (enabled && !mMethodMap.containsKey(imeId)) {
+ failedToEnableUnknownIme = true;
+ } else {
+ previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled);
+ }
+ } else {
+ final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
+ final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap,
+ userId, false);
+ if (enabled) {
+ if (!methodMap.containsKey(imeId)) {
+ failedToEnableUnknownIme = true;
+ } else {
+ for (InputMethodInfo imi : settings.getEnabledInputMethodListLocked()) {
+ if (TextUtils.equals(imi.getId(), imeId)) {
+ previouslyEnabled = true;
+ break;
+ }
+ }
+ if (!previouslyEnabled) {
+ settings.appendAndPutEnabledInputMethodLocked(imeId, false);
+ }
+ }
+ } else {
+ previouslyEnabled =
+ settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ new StringBuilder(),
+ settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+ }
+ }
+ if (failedToEnableUnknownIme) {
+ error.print("Unknown input method ");
+ error.print(imeId);
+ error.println(" cannot be enabled for user #" + userId);
+ // Also print this failure into logcat for better debuggability.
+ Slog.e(TAG, "\"ime enable " + imeId + "\" for user #" + userId
+ + " failed due to its unrecognized IME ID.");
+ return false;
+ }
+ out.print("Input method ");
+ out.print(imeId);
+ out.print(": ");
+ out.print((enabled == previouslyEnabled) ? "already " : "now ");
+ out.print(enabled ? "enabled" : "disabled");
+ out.print(" for user #");
+ out.println(userId);
+ return true;
+ }
+
+ /**
+ * Handles {@code adb shell ime set}.
+ *
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandSetInputMethod(@NonNull ShellCommand shellCommand) {
+ final int userIdToBeResolved = handleOptionsForCommandsThatOnlyHaveUserOption(shellCommand);
+ final String imeId = shellCommand.getNextArgRequired();
+ boolean hasFailed = false;
+ try (PrintWriter out = shellCommand.getOutPrintWriter();
+ PrintWriter error = shellCommand.getErrPrintWriter()) {
+ synchronized (ImfLock.class) {
+ final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
+ mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ for (int userId : userIds) {
+ if (!userHasDebugPriv(userId, shellCommand)) {
+ continue;
+ }
+ boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId,
+ userId);
+ if (failedToSelectUnknownIme) {
+ error.print("Unknown input method ");
+ error.print(imeId);
+ error.print(" cannot be selected for user #");
+ error.println(userId);
+ // Also print this failure into logcat for better debuggability.
+ Slog.e(TAG, "\"ime set " + imeId + "\" for user #" + userId
+ + " failed due to its unrecognized IME ID.");
+ } else {
+ out.print("Input method ");
+ out.print(imeId);
+ out.print(" selected for user #");
+ out.println(userId);
+ }
+ hasFailed |= failedToSelectUnknownIme;
+ }
+ }
+ }
+ return hasFailed ? ShellCommandResult.FAILURE : ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell ime reset-ime}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandResetInputMethod(@NonNull ShellCommand shellCommand) {
+ final int userIdToBeResolved = handleOptionsForCommandsThatOnlyHaveUserOption(shellCommand);
+ synchronized (ImfLock.class) {
+ try (PrintWriter out = shellCommand.getOutPrintWriter()) {
+ final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
+ mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ for (int userId : userIds) {
+ if (!userHasDebugPriv(userId, shellCommand)) {
+ continue;
+ }
+ final String nextIme;
+ final List<InputMethodInfo> nextEnabledImes;
+ if (userId == mSettings.getCurrentUserId()) {
+ hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
+ 0 /* flags */, null /* resultReceiver */,
+ SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
+ mBindingController.unbindCurrentMethod();
+
+ // Enable default IMEs, disable others
+ ArrayList<InputMethodInfo> toDisable =
+ mSettings.getEnabledInputMethodListLocked();
+ ArrayList<InputMethodInfo> defaultEnabled =
+ InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList);
+ toDisable.removeAll(defaultEnabled);
+ for (InputMethodInfo info : toDisable) {
+ setInputMethodEnabledLocked(info.getId(), false);
+ }
+ for (InputMethodInfo info : defaultEnabled) {
+ setInputMethodEnabledLocked(info.getId(), true);
+ }
+ // Choose new default IME, reset to none if no IME available.
+ if (!chooseNewDefaultIMELocked()) {
+ resetSelectedInputMethodAndSubtypeLocked(null);
+ }
+ updateInputMethodsFromSettingsLocked(true /* enabledMayChange */);
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
+ getPackageManagerForUser(mContext, mSettings.getCurrentUserId()),
+ mSettings.getEnabledInputMethodListLocked());
+ nextIme = mSettings.getSelectedInputMethod();
+ nextEnabledImes = mSettings.getEnabledInputMethodListLocked();
+ } else {
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+ new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+ methodMap, methodList, DirectBootAwareness.AUTO,
+ mSettings.getEnabledInputMethodNames());
+ final InputMethodSettings settings = new InputMethodSettings(mContext,
+ methodMap, userId, false);
+
+ nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext,
+ methodList);
+ nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME(
+ nextEnabledImes).getId();
+
+ // Reset enabled IMEs.
+ settings.putEnabledInputMethodsStr("");
+ nextEnabledImes.forEach(
+ imi -> settings.appendAndPutEnabledInputMethodLocked(
+ imi.getId(), false));
+
+ // Reset selected IME.
+ settings.putSelectedInputMethod(nextIme);
+ settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ }
+ out.println("Reset current and enabled IMEs for user #" + userId);
+ out.println(" Selected: " + nextIme);
+ nextEnabledImes.forEach(ime -> out.println(" Enabled: " + ime.getId()));
+ }
+ }
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * Handles {@code adb shell cmd input_method tracing start/stop/save-for-bugreport}.
+ * @param shellCommand {@link ShellCommand} object that is handling this command.
+ * @return Exit code of the command.
+ */
+ @BinderThread
+ @ShellCommandResult
+ private int handleShellCommandTraceInputMethod(@NonNull ShellCommand shellCommand) {
+ final String cmd = shellCommand.getNextArgRequired();
+ try (PrintWriter pw = shellCommand.getOutPrintWriter()) {
+ switch (cmd) {
+ case "start":
+ ImeTracing.getInstance().startTrace(pw);
+ break; // proceed to the next step to update the IME client processes.
+ case "stop":
+ ImeTracing.getInstance().stopTrace(pw);
+ break; // proceed to the next step to update the IME client processes.
+ case "save-for-bugreport":
+ ImeTracing.getInstance().saveForBugreport(pw);
+ // no need to update the IME client processes.
+ return ShellCommandResult.SUCCESS;
+ default:
+ pw.println("Unknown command: " + cmd);
+ pw.println("Input method trace options:");
+ pw.println(" start: Start tracing");
+ pw.println(" stop: Stop tracing");
+ // no need to update the IME client processes.
+ return ShellCommandResult.FAILURE;
+ }
+ }
+ boolean isImeTraceEnabled = ImeTracing.getInstance().isEnabled();
+ ArrayMap<IBinder, ClientState> clients;
+ synchronized (ImfLock.class) {
+ clients = new ArrayMap<>(mClients);
+ }
+ for (ClientState state : clients.values()) {
+ if (state != null) {
+ state.mClient.setImeTraceEnabled(isImeTraceEnabled);
+ }
+ }
+ return ShellCommandResult.SUCCESS;
+ }
+
+ /**
+ * @param userId the actual user handle obtained by {@link UserHandle#getIdentifier()}
+ * and *not* pseudo ids like {@link UserHandle#USER_ALL etc}.
+ * @return {@code true} if userId has debugging privileges.
+ * i.e. {@link UserManager#DISALLOW_DEBUGGING_FEATURES} is {@code false}.
+ */
+ private boolean userHasDebugPriv(@UserIdInt int userId, ShellCommand shellCommand) {
+ if (mUserManagerInternal.hasUserRestriction(
+ UserManager.DISALLOW_DEBUGGING_FEATURES, userId)) {
+ shellCommand.getErrPrintWriter().println("User #" + userId
+ + " is restricted with DISALLOW_DEBUGGING_FEATURES.");
+ return false;
+ }
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public IImeTracker getImeTrackerService() {
+ return mImeTrackerService;
+ }
+
+ /**
+ * Creates an IME request tracking token for the current focused client.
+ *
+ * @param show whether this is a show or a hide request.
+ * @param origin the origin of the IME request.
+ * @param reason the reason why the IME request was created.
+ */
+ @NonNull
+ private ImeTracker.Token createStatsTokenForFocusedClient(boolean show,
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ final int uid = mCurFocusedWindowClient != null
+ ? mCurFocusedWindowClient.mUid
+ : -1;
+ final String packageName = mCurFocusedWindowEditorInfo != null
+ ? mCurFocusedWindowEditorInfo.packageName
+ : "uid(" + uid + ")";
+
+ if (show) {
+ return ImeTracker.forLogging().onRequestShow(packageName, uid, origin, reason);
+ } else {
+ return ImeTracker.forLogging().onRequestHide(packageName, uid, origin, reason);
+ }
+ }
+
+ private static final class InputMethodPrivilegedOperationsImpl
+ extends IInputMethodPrivilegedOperations.Stub {
+ private final CarInputMethodManagerService mImms;
+ @NonNull
+ private final IBinder mToken;
+ InputMethodPrivilegedOperationsImpl(CarInputMethodManagerService imms,
+ @NonNull IBinder token) {
+ mImms = imms;
+ mToken = token;
+ }
+
+ @BinderThread
+ @Override
+ public void setImeWindowStatusAsync(int vis, int backDisposition) {
+ mImms.setImeWindowStatus(mToken, vis, backDisposition);
+ }
+
+ @BinderThread
+ @Override
+ public void reportStartInputAsync(IBinder startInputToken) {
+ mImms.reportStartInput(mToken, startInputToken);
+ }
+
+ @BinderThread
+ @Override
+ public void createInputContentUriToken(Uri contentUri, String packageName,
+ AndroidFuture future /* T=IBinder */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<IBinder> typedFuture = future;
+ try {
+ typedFuture.complete(mImms.createInputContentUriToken(
+ mToken, contentUri, packageName).asBinder());
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void reportFullscreenModeAsync(boolean fullscreen) {
+ mImms.reportFullscreenMode(mToken, fullscreen);
+ }
+
+ @BinderThread
+ @Override
+ public void setInputMethod(String id, AndroidFuture future /* T=Void */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Void> typedFuture = future;
+ try {
+ mImms.setInputMethod(mToken, id);
+ typedFuture.complete(null);
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype,
+ AndroidFuture future /* T=Void */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Void> typedFuture = future;
+ try {
+ mImms.setInputMethodAndSubtype(mToken, id, subtype);
+ typedFuture.complete(null);
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason,
+ AndroidFuture future /* T=Void */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Void> typedFuture = future;
+ try {
+ mImms.hideMySoftInput(mToken, flags, reason);
+ typedFuture.complete(null);
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void showMySoftInput(int flags, AndroidFuture future /* T=Void */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Void> typedFuture = future;
+ try {
+ mImms.showMySoftInput(mToken, flags);
+ typedFuture.complete(null);
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void updateStatusIconAsync(String packageName, @DrawableRes int iconId) {
+ mImms.updateStatusIcon(mToken, packageName, iconId);
+ }
+
+ @BinderThread
+ @Override
+ public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Boolean> typedFuture = future;
+ try {
+ typedFuture.complete(mImms.switchToPreviousInputMethod(mToken));
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void switchToNextInputMethod(boolean onlyCurrentIme,
+ AndroidFuture future /* T=Boolean */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Boolean> typedFuture = future;
+ try {
+ typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme));
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) {
+ @SuppressWarnings("unchecked")
+ final AndroidFuture<Boolean> typedFuture = future;
+ try {
+ typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken));
+ } catch (Throwable e) {
+ typedFuture.completeExceptionally(e);
+ }
+ }
+
+ @BinderThread
+ @Override
+ public void notifyUserActionAsync() {
+ mImms.notifyUserAction(mToken);
+ }
+
+ @BinderThread
+ @Override
+ public void applyImeVisibilityAsync(IBinder windowToken, boolean setVisible,
+ @Nullable ImeTracker.Token statsToken) {
+ mImms.applyImeVisibility(mToken, windowToken, setVisible, statsToken);
+ }
+
+ @BinderThread
+ @Override
+ public void onStylusHandwritingReady(int requestId, int pid) {
+ mImms.onStylusHandwritingReady(requestId, pid);
+ }
+
+ @BinderThread
+ @Override
+ public void resetStylusHandwriting(int requestId) {
+ mImms.resetStylusHandwriting(requestId);
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodMenuController.java b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodMenuController.java
new file mode 100644
index 0000000..7c110d8
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/CarInputMethodMenuController.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import static com.android.server.inputmethod.CarInputMethodManagerService.DEBUG;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
+import android.annotation.Nullable;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.ArrayAdapter;
+import android.widget.RadioButton;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.LocalServices;
+import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.util.List;
+
+/** A controller to show/hide the input method menu */
+final class CarInputMethodMenuController {
+ private static final String TAG = CarInputMethodMenuController.class.getSimpleName();
+
+ private final CarInputMethodManagerService mService;
+ private final InputMethodUtils.InputMethodSettings mSettings;
+ private final InputMethodSubtypeSwitchingController mSwitchingController;
+ private final ArrayMap<String, InputMethodInfo> mMethodMap;
+ private final WindowManagerInternal mWindowManagerInternal;
+
+ private AlertDialog.Builder mDialogBuilder;
+ private AlertDialog mSwitchingDialog;
+ private View mSwitchingDialogTitleView;
+ private InputMethodInfo[] mIms;
+ private int[] mSubtypeIds;
+
+ private boolean mShowImeWithHardKeyboard;
+
+ @GuardedBy("ImfLock.class")
+ @Nullable
+ private InputMethodDialogWindowContext mDialogWindowContext;
+
+ CarInputMethodMenuController(CarInputMethodManagerService service) {
+ mService = service;
+ mSettings = mService.mSettings;
+ mSwitchingController = mService.mSwitchingController;
+ mMethodMap = mService.mMethodMap;
+ mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
+ }
+
+ void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
+ if (DEBUG) Slog.v(TAG, "Show switching menu. showAuxSubtypes=" + showAuxSubtypes);
+
+ final boolean isScreenLocked = isScreenLocked();
+
+ final String lastInputMethodId = mSettings.getSelectedInputMethod();
+ int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+ if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
+
+ synchronized (ImfLock.class) {
+ final List<ImeSubtypeListItem> imList = mSwitchingController
+ .getSortedInputMethodAndSubtypeListForImeMenuLocked(
+ showAuxSubtypes, isScreenLocked);
+ if (imList.isEmpty()) {
+ return;
+ }
+
+ hideInputMethodMenuLocked();
+
+ if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
+ final InputMethodSubtype currentSubtype =
+ mService.getCurrentInputMethodSubtypeLocked();
+ if (currentSubtype != null) {
+ final String curMethodId = mService.getSelectedMethodIdLocked();
+ final InputMethodInfo currentImi = mMethodMap.get(curMethodId);
+ lastInputMethodSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
+ currentImi, currentSubtype.hashCode());
+ }
+ }
+
+ final int size = imList.size();
+ mIms = new InputMethodInfo[size];
+ mSubtypeIds = new int[size];
+ int checkedItem = 0;
+ for (int i = 0; i < size; ++i) {
+ final ImeSubtypeListItem item = imList.get(i);
+ mIms[i] = item.mImi;
+ mSubtypeIds[i] = item.mSubtypeId;
+ if (mIms[i].getId().equals(lastInputMethodId)) {
+ int subtypeId = mSubtypeIds[i];
+ if ((subtypeId == NOT_A_SUBTYPE_ID)
+ || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
+ || (subtypeId == lastInputMethodSubtypeId)) {
+ checkedItem = i;
+ }
+ }
+ }
+
+ if (mDialogWindowContext == null) {
+ mDialogWindowContext = new InputMethodDialogWindowContext();
+ }
+ final Context dialogWindowContext = mDialogWindowContext.get(displayId);
+ mDialogBuilder = new AlertDialog.Builder(dialogWindowContext);
+ mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
+
+ final Context dialogContext = mDialogBuilder.getContext();
+ final TypedArray a = dialogContext.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.DialogPreference,
+ com.android.internal.R.attr.alertDialogStyle, 0);
+ final Drawable dialogIcon = a.getDrawable(
+ com.android.internal.R.styleable.DialogPreference_dialogIcon);
+ a.recycle();
+
+ mDialogBuilder.setIcon(dialogIcon);
+
+ final LayoutInflater inflater = dialogContext.getSystemService(LayoutInflater.class);
+ final View tv = inflater.inflate(
+ com.android.internal.R.layout.input_method_switch_dialog_title, null);
+ mDialogBuilder.setCustomTitle(tv);
+
+ // Setup layout for a toggle switch of the hardware keyboard
+ mSwitchingDialogTitleView = tv;
+ mSwitchingDialogTitleView
+ .findViewById(com.android.internal.R.id.hard_keyboard_section)
+ .setVisibility(mWindowManagerInternal.isHardKeyboardAvailable()
+ ? View.VISIBLE : View.GONE);
+ final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_switch);
+ hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
+ hardKeySwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ mSettings.setShowImeWithHardKeyboard(isChecked);
+ // Ensure that the input method dialog is dismissed when changing
+ // the hardware keyboard state.
+ hideInputMethodMenu();
+ });
+
+ final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(dialogContext,
+ com.android.internal.R.layout.input_method_switch_item, imList, checkedItem);
+ final DialogInterface.OnClickListener choiceListener = (dialog, which) -> {
+ synchronized (ImfLock.class) {
+ if (mIms == null || mIms.length <= which || mSubtypeIds == null
+ || mSubtypeIds.length <= which) {
+ return;
+ }
+ final InputMethodInfo im = mIms[which];
+ int subtypeId = mSubtypeIds[which];
+ adapter.mCheckedItem = which;
+ adapter.notifyDataSetChanged();
+ hideInputMethodMenu();
+ if (im != null) {
+ if (subtypeId < 0 || subtypeId >= im.getSubtypeCount()) {
+ subtypeId = NOT_A_SUBTYPE_ID;
+ }
+ mService.setInputMethodLocked(im.getId(), subtypeId);
+ }
+ }
+ };
+ mDialogBuilder.setSingleChoiceItems(adapter, checkedItem, choiceListener);
+
+ mSwitchingDialog = mDialogBuilder.create();
+ mSwitchingDialog.setCanceledOnTouchOutside(true);
+ final Window w = mSwitchingDialog.getWindow();
+ final WindowManager.LayoutParams attrs = w.getAttributes();
+ w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
+ w.setHideOverlayWindows(true);
+ // Use an alternate token for the dialog for that window manager can group the token
+ // with other IME windows based on type vs. grouping based on whichever token happens
+ // to get selected by the system later on.
+ attrs.token = dialogWindowContext.getWindowContextToken();
+ attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ attrs.setTitle("Select input method");
+ w.setAttributes(attrs);
+ mService.updateSystemUiLocked();
+ mService.sendOnNavButtonFlagsChangedLocked();
+ mSwitchingDialog.show();
+ }
+ }
+
+ private boolean isScreenLocked() {
+ return mWindowManagerInternal.isKeyguardLocked()
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId());
+ }
+
+ void updateKeyboardFromSettingsLocked() {
+ mShowImeWithHardKeyboard = mSettings.isShowImeWithHardKeyboardEnabled();
+ if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
+ && mSwitchingDialog.isShowing()) {
+ final Switch hardKeySwitch = mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_switch);
+ hardKeySwitch.setChecked(mShowImeWithHardKeyboard);
+ }
+ }
+
+ void hideInputMethodMenu() {
+ synchronized (ImfLock.class) {
+ hideInputMethodMenuLocked();
+ }
+ }
+
+ @GuardedBy("ImfLock.class")
+ void hideInputMethodMenuLocked() {
+ if (DEBUG) Slog.v(TAG, "Hide switching menu");
+
+ if (mSwitchingDialog != null) {
+ mSwitchingDialog.dismiss();
+ mSwitchingDialog = null;
+ mSwitchingDialogTitleView = null;
+
+ mService.updateSystemUiLocked();
+ mService.sendOnNavButtonFlagsChangedLocked();
+ mDialogBuilder = null;
+ mIms = null;
+ }
+ }
+
+ AlertDialog getSwitchingDialogLocked() {
+ return mSwitchingDialog;
+ }
+
+ boolean getShowImeWithHardKeyboard() {
+ return mShowImeWithHardKeyboard;
+ }
+
+ boolean isisInputMethodPickerShownForTestLocked() {
+ if (mSwitchingDialog == null) {
+ return false;
+ }
+ return mSwitchingDialog.isShowing();
+ }
+
+ void handleHardKeyboardStatusChange(boolean available) {
+ if (DEBUG) {
+ Slog.w(TAG, "HardKeyboardStatusChanged: available=" + available);
+ }
+ synchronized (ImfLock.class) {
+ if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
+ && mSwitchingDialog.isShowing()) {
+ mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_section).setVisibility(
+ available ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+
+ private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> {
+ private final LayoutInflater mInflater;
+ private final int mTextViewResourceId;
+ private final List<ImeSubtypeListItem> mItemsList;
+ public int mCheckedItem;
+ private ImeSubtypeListAdapter(Context context, int textViewResourceId,
+ List<ImeSubtypeListItem> itemsList, int checkedItem) {
+ super(context, textViewResourceId, itemsList);
+
+ mTextViewResourceId = textViewResourceId;
+ mItemsList = itemsList;
+ mCheckedItem = checkedItem;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = convertView != null ? convertView
+ : mInflater.inflate(mTextViewResourceId, null);
+ if (position < 0 || position >= mItemsList.size()) return view;
+ final ImeSubtypeListItem item = mItemsList.get(position);
+ final CharSequence imeName = item.mImeName;
+ final CharSequence subtypeName = item.mSubtypeName;
+ final TextView firstTextView = view.findViewById(android.R.id.text1);
+ final TextView secondTextView = view.findViewById(android.R.id.text2);
+ if (TextUtils.isEmpty(subtypeName)) {
+ firstTextView.setText(imeName);
+ secondTextView.setVisibility(View.GONE);
+ } else {
+ firstTextView.setText(subtypeName);
+ secondTextView.setText(imeName);
+ secondTextView.setVisibility(View.VISIBLE);
+ }
+ final RadioButton radioButton = view.findViewById(com.android.internal.R.id.radio);
+ radioButton.setChecked(position == mCheckedItem);
+ return view;
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/InputMethodManagerServiceProxy.java b/builtInServices/src_imms/com/android/server/inputmethod/InputMethodManagerServiceProxy.java
new file mode 100644
index 0000000..e7bb329
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/InputMethodManagerServiceProxy.java
@@ -0,0 +1,1133 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.inputmethod;
+
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL;
+import static android.os.IServiceManager.DUMP_FLAG_PROTO;
+
+import android.annotation.BinderThread;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.window.ImeOnBackInvokedDispatcher;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
+import com.android.internal.inputmethod.IImeTracker;
+import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
+import com.android.internal.inputmethod.IInputMethodClient;
+import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
+import com.android.internal.inputmethod.InputBindResult;
+import com.android.internal.inputmethod.SoftInputShowHideReason;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.internal.view.IInputMethodManager;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Proxy used to host IMMSs per user and reroute requests to the user associated IMMS.
+ *
+ * @hide
+ */
+public final class InputMethodManagerServiceProxy extends IInputMethodManager.Stub {
+
+ private static final String IMMS_TAG = InputMethodManagerServiceProxy.class.getSimpleName();
+ private static final boolean DBG = Log.isLoggable(IMMS_TAG, Log.DEBUG);
+
+ // System property used to disable IMMS proxy.
+ // When set to true, Android Core's original IMMS will be launched instead.
+ // Note: this flag only takes effects on non user builds.
+ public static final String DISABLE_MU_IMMS = "persist.fw.car.test.disable_mu_imms";
+
+ private static final ExecutorService sExecutor = Executors.newCachedThreadPool();
+
+ private final ReentrantReadWriteLock mRwLock = new ReentrantReadWriteLock();
+
+ @GuardedBy("mRwLock")
+ private final SparseArray<CarInputMethodManagerService> mServicesForUser = new SparseArray<>();
+
+ @GuardedBy("mRwLock")
+ private final SparseArray<InputMethodManagerInternal> mLocalServicesForUser =
+ new SparseArray<>();
+
+ private final Context mContext;
+ private InputMethodManagerInternalProxy mInternalProxy;
+
+ public InputMethodManagerServiceProxy(Context context) {
+ mContext = context;
+ mInternalProxy = new InputMethodManagerInternalProxy();
+ }
+
+ @UserIdInt
+ private int getCallingUserId() {
+ final int uid = Binder.getCallingUid();
+ return UserHandle.getUserId(uid);
+ }
+
+ InputMethodManagerInternal getLocalServiceProxy() {
+ return mInternalProxy;
+ }
+
+ CarInputMethodManagerService createAndRegisterServiceFor(@UserIdInt int userId) {
+ Slogf.d(IMMS_TAG, "Starting IMMS and IMMI for user {%d}", userId);
+ CarInputMethodManagerService imms;
+ try {
+ mRwLock.writeLock().lock();
+ if ((imms = mServicesForUser.get(userId)) != null) {
+ return imms;
+ }
+ imms = new CarInputMethodManagerService(mContext, sExecutor);
+ mServicesForUser.set(userId, imms);
+ InputMethodManagerInternal localService = imms.getInputMethodManagerInternal();
+ mLocalServicesForUser.set(userId, localService);
+ } finally {
+ mRwLock.writeLock().unlock();
+ }
+ imms.systemRunning();
+ Slogf.d(IMMS_TAG, "Started IMMS and IMMI for user {%d}", userId);
+ return imms;
+ }
+
+ CarInputMethodManagerService getServiceForUser(@UserIdInt int userId) {
+ try {
+ mRwLock.readLock().lock();
+ return mServicesForUser.get(userId);
+ } finally {
+ mRwLock.readLock().unlock();
+ }
+ }
+
+ InputMethodManagerInternal getLocalServiceForUser(@UserIdInt int userId) {
+ try {
+ mRwLock.readLock().lock();
+ return mLocalServicesForUser.get(userId);
+ } finally {
+ mRwLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * SystemService for CarInputMethodManagerServices.
+ *
+ * If {@code fw.enable_imms_proxy} system property is set to {@code false}, then it just
+ * delegate to Android Core original {@link InputMethodManagerService.Lifecycle}.
+ *
+ * TODO(b/245798405): make Lifecycle class easier to test and add tests for it
+ */
+ public static class Lifecycle extends SystemService {
+ private static final String LIFECYCLE_TAG =
+ IMMS_TAG + "." + Lifecycle.class.getSimpleName();
+
+ private final InputMethodManagerServiceProxy mServiceProxy;
+ private final Context mContext;
+ private final UserManagerInternal mUserManagerInternal;
+ private HandlerThread mWorkerThread;
+ private Handler mHandler;
+
+ // Android core IMMS to be used when IMMS Proxy is disabled
+ private final InputMethodManagerService.Lifecycle mCoreImmsLifecycle;
+
+ /**
+ * Initializes the system service for InputMethodManagerServiceProxy.
+ */
+ public Lifecycle(@NonNull Context context) {
+ super(context);
+ mContext = context;
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ mServiceProxy = new InputMethodManagerServiceProxy(mContext);
+ if (!Build.IS_USER && SystemProperties.getBoolean(
+ DISABLE_MU_IMMS, /* defaultValue= */ false)) {
+ mCoreImmsLifecycle = new InputMethodManagerService.Lifecycle(mContext);
+ } else {
+ mCoreImmsLifecycle = null;
+ }
+ }
+
+ private boolean isImmsProxyEnabled() {
+ return mCoreImmsLifecycle == null;
+ }
+
+ @MainThread
+ @Override
+ public void onStart() {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG, "Entering #onStart (IMMS Proxy enabled={%s})",
+ isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onStart();
+ return;
+ }
+ mWorkerThread = new HandlerThread(IMMS_TAG);
+ mWorkerThread.start();
+ mHandler = new Handler(mWorkerThread.getLooper(), msg -> false, true);
+
+ // Register broadcast receivers for user state changes
+ IntentFilter broadcastFilterForSystemUser = new IntentFilter();
+ broadcastFilterForSystemUser.addAction(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(new ImmsBroadcastReceiverForSystemUser(),
+ broadcastFilterForSystemUser);
+
+ // Register binders
+ LocalServices.addService(InputMethodManagerInternal.class,
+ mServiceProxy.getLocalServiceProxy());
+ publishBinderService(Context.INPUT_METHOD_SERVICE, mServiceProxy,
+ false /*allowIsolated*/,
+ DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);
+ }
+
+ @MainThread
+ @Override
+ public void onBootPhase(int phase) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG,
+ "Entering #onBootPhase with phase={%d} (IMMS Proxy enabled={%s})", phase,
+ isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onBootPhase(phase);
+ return;
+ }
+ if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+ mHandler.sendMessage(PooledLambda.obtainMessage(
+ Lifecycle::onBootPhaseReceived, this, phase));
+ }
+ }
+
+ @WorkerThread
+ private void onBootPhaseReceived(int phase) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG, "Entering #onBootPhaseReceived with phase={%d}", phase);
+ }
+ int[] userIds = mUserManagerInternal.getUserIds();
+ for (int i = 0; i < userIds.length; ++i) {
+ mServiceProxy.createAndRegisterServiceFor(userIds[i]);
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onUserStarting(@NonNull TargetUser user) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG,
+ "Entering #onUserStarting with user={%s} (IMMS Proxy enabled={%s})", user,
+ isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onUserStarting(user);
+ return;
+ }
+ mHandler.sendMessage(PooledLambda.obtainMessage(
+ Lifecycle::onUserStartingReceived, this, user));
+ }
+
+ @WorkerThread
+ private void onUserStartingReceived(@NonNull TargetUser user) {
+ // This method may be invoked under WindowManagerGlobalLock, therefore the code must be
+ // run on separated thread to avoid deadlock (imms#systemRUnning and
+ // imms#scheduleSwitchUserTaskLocked will try to acquire WindowManagerGlobalLock).
+ sExecutor.execute(() -> {
+ CarInputMethodManagerService imms = mServiceProxy.createAndRegisterServiceFor(
+ user.getUserIdentifier());
+ synchronized (ImfLock.class) {
+ imms.scheduleSwitchUserTaskLocked(user.getUserIdentifier(),
+ /* clientToBeReset= */ null);
+ }
+ });
+ }
+
+ @MainThread
+ @Override
+ public void onUserUnlocking(@NonNull TargetUser user) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG,
+ "Entering #onUserUnlockingReceived with to={%s} (IMMS Proxy enabled={%s})",
+ user, isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onUserUnlocking(user);
+ return;
+ }
+ mHandler.sendMessage(PooledLambda.obtainMessage(
+ Lifecycle::onUserUnlockingReceived, this, user));
+ }
+
+ @WorkerThread
+ private void onUserUnlockingReceived(@NonNull TargetUser user) {
+ CarInputMethodManagerService service = mServiceProxy.getServiceForUser(
+ user.getUserIdentifier());
+ if (service != null) {
+ // Called on ActivityManager thread.
+ service.notifySystemUnlockUser(user.getUserIdentifier());
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onUserStopping(@NonNull TargetUser user) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG,
+ "Entering #onUserStoppingReceived with userId={%d} (IMMS Proxy "
+ + "enabled={%s})",
+ user.getUserIdentifier(), isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onUserStopping(user);
+ return;
+ }
+ sExecutor.execute(
+ () -> mServiceProxy.removeCarInputMethodManagerServiceForUser(
+ user.getUserIdentifier()));
+ }
+
+ @MainThread
+ @Override
+ public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+ if (DBG) {
+ Slogf.d(LIFECYCLE_TAG,
+ "Entering #onUserSwitching with from={%d} and to={%d} (IMMS Proxy "
+ + "enabled={%s})",
+ from.getUserIdentifier(), to.getUserIdentifier(), isImmsProxyEnabled());
+ }
+ if (!isImmsProxyEnabled()) {
+ mCoreImmsLifecycle.onUserSwitching(from, to);
+ }
+ }
+
+ /**
+ * {@link BroadcastReceiver} that is intended to listen to broadcasts sent to the system
+ * user only.
+ */
+ private final class ImmsBroadcastReceiverForSystemUser extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
+ mServiceProxy.onActionLocaleChanged();
+ } else {
+ Slogf.w(LIFECYCLE_TAG, "Unexpected intent " + intent);
+ }
+ }
+ }
+ }
+
+ private void removeCarInputMethodManagerServiceForUser(int userId) {
+ mRwLock.writeLock().lock();
+ try {
+ CarInputMethodManagerService imms = mServicesForUser.get(userId);
+ if (imms == null) {
+ return;
+ }
+ mServicesForUser.remove(userId);
+ mLocalServicesForUser.remove(userId);
+ imms.systemShutdown();
+ } finally {
+ mRwLock.writeLock().unlock();
+ }
+ Slogf.i(IMMS_TAG, "Removed CarIMMS for user {%d}", userId);
+ }
+
+ /**
+ * Dump this IMMS Proxy object. If `--user` arg is pass (along with an existing user id) then
+ * it will just dump the user associated IMMS.
+ */
+ @BinderThread
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, IMMS_TAG, pw)) {
+ Slogf.w(IMMS_TAG, "Permission denied for #dump");
+ return;
+ }
+
+ // Check if --user is set. If set, then just dump the user's IMMS.
+ int userIdArg = parseUserArgIfPresent(args);
+ if (userIdArg != UserHandle.USER_NULL) {
+ mServicesForUser.get(userIdArg).dump(fd, pw, args);
+ return;
+ }
+ pw.println("*InputMethodManagerServiceProxy");
+ pw.println("**mServicesForUser**");
+ try {
+ mRwLock.readLock().lock();
+ if (parseBriefArg(args)) {
+ // Dump brief
+ for (int i = 0; i < mServicesForUser.size(); i++) {
+ int userId = mServicesForUser.keyAt(i);
+ CarInputMethodManagerService imms = mServicesForUser.valueAt(i);
+ pw.println(" userId=" + userId + " imms=" + imms.hashCode() + " {autofill="
+ + imms.getAutofillController() + "}");
+ }
+ pw.println("**mLocalServicesForUser**");
+ for (int i = 0; i < mLocalServicesForUser.size(); i++) {
+ int userId = mLocalServicesForUser.keyAt(i);
+ InputMethodManagerInternal immi = mLocalServicesForUser.valueAt(i);
+ pw.println(" userId=" + userId + " immi=" + immi.hashCode());
+ }
+ } else {
+ // Dump full
+ for (int i = 0; i < mServicesForUser.size(); i++) {
+ int userId = mServicesForUser.keyAt(i);
+ pw.println("**CarInputMethodManagerService for userId=" + userId);
+ CarInputMethodManagerService imms = mServicesForUser.valueAt(i);
+ imms.dump(fd, pw, args);
+ }
+ }
+ } finally {
+ mRwLock.readLock().unlock();
+ }
+ pw.flush();
+ }
+
+ private boolean parseBriefArg(String[] args) {
+ if (args == null) {
+ return false;
+ }
+ for (int i = 0; i < args.length; i++) {
+ if (args[i].equals("--brief")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Parse the args string and returns the value of `--user` argument. Returns
+ * {@link UserHandle.USER_NULL} in case of `--user` is not in args.
+ *
+ * @return the value of `--user` argument or UserHandle.USER_NULL if `--user` is not in args
+ * @throws IllegalArgumentException if `--user` arg is not passed along a user id
+ * @throws NumberFormatException if the value passed along `--user` is not an integer
+ */
+ private int parseUserArgIfPresent(String[] args) {
+ if (args == null) {
+ return UserHandle.USER_NULL;
+ }
+ for (int i = 0; i < args.length; ++i) {
+ if ("--user".equals(args[i])) {
+ if (i == args.length - 1) {
+ throw new IllegalArgumentException("User id must be passed within --user arg");
+ }
+ try {
+ return Integer.parseInt(args[++i]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Expected an integer value for `--user` arg, got " + args[i]);
+ }
+ }
+ }
+ return UserHandle.USER_NULL;
+ }
+
+ private void onActionLocaleChanged() {
+ try {
+ mRwLock.readLock().lock();
+ for (int i = 0; i < mServicesForUser.size(); i++) {
+ int userId = mServicesForUser.keyAt(i);
+ Slogf.i(IMMS_TAG, "Updating location for user {%d} Car IMMS", userId);
+ CarInputMethodManagerService imms = mServicesForUser.valueAt(i);
+ imms.onActionLocaleChanged();
+ }
+ } finally {
+ mRwLock.readLock().unlock();
+ }
+ }
+
+ // Delegate methods ///////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void addClient(IInputMethodClient client, IRemoteInputConnection inputmethod,
+ int untrustedDisplayId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking addClient with untrustedDisplayId={%d}",
+ callingUserId, untrustedDisplayId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.addClient(client, inputmethod, untrustedDisplayId);
+ }
+
+ @Override
+ public List<InputMethodInfo> getInputMethodList(int userId, int directBootAwareness) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getInputMethodList with userId={%d}",
+ callingUserId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getInputMethodList(userId, directBootAwareness);
+ }
+
+ @Override
+ public List<InputMethodInfo> getEnabledInputMethodList(int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getInputMethodList with userId={%d}",
+ callingUserId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getEnabledInputMethodList(userId);
+ }
+
+ @Override
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
+ boolean allowsImplicitlyEnabledSubtypes, int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG,
+ "User {%d} invoking getEnabledInputMethodSubtypeList with imiId={%s}",
+ callingUserId, imiId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes,
+ userId);
+ }
+
+ @Override
+ public InputMethodSubtype getLastInputMethodSubtype(int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getLastInputMethodSubtype with userId={%d}",
+ callingUserId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getLastInputMethodSubtype(userId);
+ }
+
+ @Override
+ public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
+ ImeTracker.Token statsToken, int flags, int lastClickToolType,
+ ResultReceiver resultReceiver, int reason) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking showSoftInput with "
+ + "windowToken={%s} and reason={%d}", callingUserId, windowToken, reason);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType,
+ resultReceiver,
+ reason);
+ }
+
+ @Override
+ public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
+ @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking hideSoftInput with "
+ + "windowToken={%s} and reason={%d}", callingUserId, windowToken, reason);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
+ reason);
+ }
+
+ @Override
+ public InputBindResult startInputOrWindowGainedFocus(int startInputReason,
+ IInputMethodClient client, IBinder windowToken, int startInputFlags,
+ int softInputMode,
+ int windowFlags, EditorInfo editorInfo, IRemoteInputConnection inputConnection,
+ IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection,
+ int unverifiedTargetSdkVersion, int userId,
+ ImeOnBackInvokedDispatcher imeDispatcher) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking startInputOrWindowGainedFocus with "
+ + "windowToken={%s} and reason={%d}", callingUserId, windowToken, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ InputBindResult result = imms.startInputOrWindowGainedFocus(startInputReason,
+ client, windowToken, startInputFlags, softInputMode,
+ windowFlags, editorInfo, inputConnection,
+ remoteAccessibilityInputConnection, unverifiedTargetSdkVersion, userId,
+ imeDispatcher);
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "Returning {%s} for startInputOrWindowGainedFocus / user {%d}",
+ result,
+ userId);
+ }
+ return result;
+ }
+
+ @Override
+ public void showInputMethodPickerFromClient(IInputMethodClient client,
+ int auxiliarySubtypeMode) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking showInputMethodPickerFromClient",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.showInputMethodPickerFromClient(client, auxiliarySubtypeMode);
+ }
+
+ @Override
+ public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getLastInputMethodSubtype with "
+ + " auxiliarySubtypeMode={%d}, and displayId={%d}", callingUserId,
+ auxiliarySubtypeMode, displayId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId);
+ }
+
+ @Override
+ public boolean isInputMethodPickerShownForTest() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking isInputMethodPickerShownForTest",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.isInputMethodPickerShownForTest();
+ }
+
+ @Override
+ public InputMethodSubtype getCurrentInputMethodSubtype(int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG,
+ "User {%d} invoking getCurrentInputMethodSubtype with userId={%d}",
+ callingUserId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getCurrentInputMethodSubtype(userId);
+ }
+
+ @Override
+ public void setAdditionalInputMethodSubtypes(String id, InputMethodSubtype[] subtypes,
+ int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking setAdditionalInputMethodSubtypes with "
+ + "id={%d} and userId={%d}", callingUserId, id, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.setAdditionalInputMethodSubtypes(id, subtypes, userId);
+ }
+
+ @Override
+ public void setExplicitlyEnabledInputMethodSubtypes(String imeId, int[] subtypeHashCodes,
+ int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking setExplicitlyEnabledInputMethodSubtypes with "
+ + "imeId={%d} and userId={%d}", callingUserId, imeId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId);
+ }
+
+ @Override
+ public int getInputMethodWindowVisibleHeight(IInputMethodClient client) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getInputMethodWindowVisibleHeight",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getInputMethodWindowVisibleHeight(client);
+ }
+
+ @Override
+ public void reportVirtualDisplayGeometryAsync(IInputMethodClient parentClient,
+ int childDisplayId, float[] matrixValues) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking reportVirtualDisplayGeometryAsync",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.reportVirtualDisplayGeometryAsync(parentClient, childDisplayId, matrixValues);
+ }
+
+ @Override
+ public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking reportPerceptibleAsync",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.reportPerceptibleAsync(windowToken, perceptible);
+ }
+
+ @Override
+ public void removeImeSurface() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking removeImeSurface",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.removeImeSurface();
+ }
+
+ @Override
+ public void removeImeSurfaceFromWindowAsync(IBinder windowToken) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking removeImeSurfaceFromWindowAsync "
+ + "with windowToken={%s}", callingUserId, windowToken);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.removeImeSurfaceFromWindowAsync(windowToken);
+ }
+
+ @Override
+ public void startProtoDump(byte[] protoDump, int source, String where) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking startProtoDump", callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.startProtoDump(protoDump, source, where);
+ }
+
+ @Override
+ public boolean isImeTraceEnabled() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking isImeTraceEnabled", callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.isImeTraceEnabled();
+ }
+
+ @Override
+ public void startImeTrace() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking startImeTrace", callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.startImeTrace();
+ }
+
+ @Override
+ public void stopImeTrace() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking stopImeTrace", callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.stopImeTrace();
+ }
+
+ @Override
+ public void startStylusHandwriting(IInputMethodClient client) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking startStylusHandwriting", callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.startStylusHandwriting(client);
+ }
+
+ @Override
+ public void prepareStylusHandwritingDelegation(
+ @NonNull IInputMethodClient client,
+ @UserIdInt int userId,
+ @NonNull String delegatePackageName,
+ @NonNull String delegatorPackageName) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking prepareStylusHandwritingDelegation with"
+ + "client={%s}, userId={%d}, delegatePackageName={%s}, "
+ + "delegatorPackageName={%s}",
+ callingUserId, client, delegatePackageName, delegatorPackageName);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.prepareStylusHandwritingDelegation(client, userId, delegatePackageName,
+ delegatorPackageName);
+ }
+
+ @Override
+ public boolean acceptStylusHandwritingDelegation(
+ @NonNull IInputMethodClient client,
+ @UserIdInt int userId,
+ @NonNull String delegatePackageName,
+ @NonNull String delegatorPackageName) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking acceptStylusHandwritingDelegation with"
+ + "client={%s}, userId={%d}, delegatePackageName={%s}, "
+ + "delegatorPackageName={%s}",
+ callingUserId, client, delegatePackageName, delegatorPackageName);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.acceptStylusHandwritingDelegation(client, userId, delegatePackageName,
+ delegatorPackageName);
+ }
+
+ @Override
+ public boolean isStylusHandwritingAvailableAsUser(int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking isStylusHandwritingAvailableAsUser",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.isStylusHandwritingAvailableAsUser(userId);
+ }
+
+ @Override
+ public void addVirtualStylusIdForTestSession(IInputMethodClient client) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking addVirtualStylusIdForTestSession",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.addVirtualStylusIdForTestSession(client);
+ }
+
+ @Override
+ public void setStylusWindowIdleTimeoutForTest(IInputMethodClient client, long timeout) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking setStylusWindowIdleTimeoutForTest",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ imms.setStylusWindowIdleTimeoutForTest(client, timeout);
+ }
+
+ @Override
+ public IImeTracker getImeTrackerService() {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getImeTrackerService",
+ callingUserId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getImeTrackerService();
+ }
+
+ @Override
+ public InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) {
+ final int callingUserId = getCallingUserId();
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "User {%d} invoking getCurrentInputMethodInfoAsUser with userId={%d}",
+ callingUserId, userId);
+ }
+ CarInputMethodManagerService imms = getServiceForUser(callingUserId);
+ return imms.getCurrentInputMethodInfoAsUser(userId);
+ }
+
+ @BinderThread
+ @Override
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err,
+ @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ checkCallerIsRootOrShell(args, resultReceiver);
+ int userId;
+ try {
+ userId = parseUserArgIfPresent(args);
+ } catch (IllegalArgumentException | SecurityException e) {
+ resultReceiver.send(-1 /* FAILURE */, null);
+ Slogf.e(IMMS_TAG, "Failed parsing incoming shell command", e);
+ return;
+ }
+ if (userId == UserHandle.USER_NULL) {
+ Slogf.w(IMMS_TAG, "Ignoring incoming shell command {%s}, "
+ + "no user was specified (use --user flag to specify the user id)",
+ Arrays.toString(args));
+ resultReceiver.send(-1 /* FAILURE */, null);
+ return;
+ }
+ CarInputMethodManagerService imms = getServiceForUser(userId);
+ if (imms == null) {
+ Slogf.e(IMMS_TAG, String.format("Ignoring incoming shell command {%s},"
+ + " there is no Car IMMS for user {%d}", Arrays.toString(args), userId));
+ resultReceiver.send(-1 /* FAILURE */, null);
+ return;
+ }
+ if (DBG) {
+ Slogf.d(IMMS_TAG, "Running shell command {%s} on imms {%d}", Arrays.toString(args),
+ userId);
+ }
+ imms.onShellCommand(in, out, err, args, callback, resultReceiver);
+ resultReceiver.send(0 /* SUCCESS */, null);
+ }
+
+ private void checkCallerIsRootOrShell(String[] args, @NonNull ResultReceiver resultReceiver)
+ throws SecurityException {
+ final int callingUid = Binder.getCallingUid();
+ // Regular adb shell will come with process SHELL_UID and adb root shell with ROOT_UID
+ if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) {
+ resultReceiver.send(-1 /* FAILURE */, null);
+ String errorMsg = String.format("InputMethodManagerServiceProxy does not support"
+ + " shell commands from non-shell users. callingUid={%d} args={%s}",
+ callingUid, Arrays.toString(args));
+ if (Process.isCoreUid(callingUid)) {
+ // Let's not crash the calling process if the caller is one of core components
+ // (this is the same logic adopted by Android Core's IMMS).
+ Slogf.e(IMMS_TAG, errorMsg);
+ return;
+ }
+ throw new SecurityException(errorMsg);
+ }
+ }
+
+ class InputMethodManagerInternalProxy extends InputMethodManagerInternal {
+ private final String mImmiTag =
+ IMMS_TAG + "." + InputMethodManagerInternalProxy.class.getSimpleName();
+
+ @Override
+ public void setInteractive(boolean interactive) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking setInteractive", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.setInteractive(interactive);
+ }
+
+ @Override
+ public void hideCurrentInputMethod(int reason) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking hideCurrentInputMethod", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.hideCurrentInputMethod(reason);
+ }
+
+ @Override
+ public List<InputMethodInfo> getInputMethodListAsUser(int userId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking getInputMethodListAsUser=%d",
+ callingUserId,
+ userId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ return immi.getInputMethodListAsUser(userId);
+ }
+
+ @Override
+ public List<InputMethodInfo> getEnabledInputMethodListAsUser(int userId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking getEnabledInputMethodListAsUser=%d",
+ callingUserId, userId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ return immi.getEnabledInputMethodListAsUser(userId);
+ }
+
+ @Override
+ public void onCreateInlineSuggestionsRequest(int userId,
+ InlineSuggestionsRequestInfo requestInfo,
+ IInlineSuggestionsRequestCallback cb) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking onCreateInlineSuggestionsRequest=%d",
+ callingUserId, userId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.onCreateInlineSuggestionsRequest(userId, requestInfo, cb);
+ }
+
+ @Override
+ public boolean switchToInputMethod(String imeId, int userId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking switchToInputMethod=%d", callingUserId,
+ userId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ return immi.switchToInputMethod(imeId, userId);
+ }
+
+ @Override
+ public boolean setInputMethodEnabled(String imeId, boolean enabled, int userId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking setInputMethodEnabled", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ return immi.setInputMethodEnabled(imeId, enabled, userId);
+ }
+
+ @Override
+ public void registerInputMethodListListener(InputMethodListListener listener) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking registerInputMethodListListener",
+ callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.registerInputMethodListListener(listener);
+ }
+
+ @Override
+ public boolean transferTouchFocusToImeWindow(
+ @NonNull IBinder sourceInputToken, int displayId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking transferTouchFocusToImeWindow",
+ callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ return immi.transferTouchFocusToImeWindow(sourceInputToken, displayId);
+ }
+
+ @Override
+ public void reportImeControl(@Nullable IBinder windowToken) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking reportImeControl", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.reportImeControl(windowToken);
+ }
+
+ @Override
+ public void onImeParentChanged() {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking onImeParentChanged", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.onImeParentChanged();
+ }
+
+ @Override
+ public void removeImeSurface() {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking removeImeSurface", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.removeImeSurface();
+ }
+
+ @Override
+ public void updateImeWindowStatus(boolean disableImeIcon) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking updateImeWindowStatus", callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.updateImeWindowStatus(disableImeIcon);
+ }
+
+ @Override
+ public void maybeFinishStylusHandwriting() {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking maybeFinishStylusHandwriting",
+ callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.maybeFinishStylusHandwriting();
+ }
+
+ @Override
+ public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
+ IAccessibilityInputMethodSession session) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking onSessionForAccessibilityCreated",
+ callingUserId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.onSessionForAccessibilityCreated(accessibilityConnectionId, session);
+ }
+
+ @Override
+ public void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking unbindAccessibilityFromCurrentClient("
+ + "accessibilityConnectionId=%d)", callingUserId,
+ accessibilityConnectionId);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.unbindAccessibilityFromCurrentClient(accessibilityConnectionId);
+ }
+
+ @Override
+ public void switchKeyboardLayout(int direction) {
+ final int uid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(uid);
+ if (DBG) {
+ Slogf.d(mImmiTag, "User {%d} invoking switchKeyboardLayout(direction=%d)",
+ callingUserId, direction);
+ }
+ InputMethodManagerInternal immi = getLocalServiceForUser(callingUserId);
+ immi.switchKeyboardLayout(direction);
+ }
+ }
+}
diff --git a/builtInServices/src_imms/com/android/server/inputmethod/NullAutofillSuggestionsController.java b/builtInServices/src_imms/com/android/server/inputmethod/NullAutofillSuggestionsController.java
new file mode 100644
index 0000000..87314c0
--- /dev/null
+++ b/builtInServices/src_imms/com/android/server/inputmethod/NullAutofillSuggestionsController.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
+import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
+
+/**
+ * A null implementation of {@link AutofillController}.
+ */
+public class NullAutofillSuggestionsController implements AutofillController {
+
+ @Override
+ public void onCreateInlineSuggestionsRequest(int userId,
+ InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
+ boolean touchExplorationEnabled) {
+ // Do nothing.
+ }
+
+ @Override
+ public void performOnCreateInlineSuggestionsRequest() {
+ // Do nothing.
+ }
+
+ @Override
+ public void invalidateAutofillSession() {
+ // Do nothing.
+ }
+}
diff --git a/builtInServices/tests/Android.bp b/builtInServices/tests/Android.bp
new file mode 100644
index 0000000..eb0346f
--- /dev/null
+++ b/builtInServices/tests/Android.bp
@@ -0,0 +1,48 @@
+package {
+ default_applicable_licenses: [
+ "Android-Apache-2.0",
+ ],
+}
+
+android_test {
+ name: "FrameworkOptCarServicesTest",
+
+ srcs: [
+ "src/**/*.java"
+ ],
+
+ platform_apis: true,
+
+ certificate: "platform",
+
+ optimize: {
+ enabled: false,
+ },
+
+ libs: [
+ "android.car",
+ "android.car.builtin",
+ "android.test.runner",
+ "android.test.base",
+ "android.hardware.automotive.vehicle-V2.0-java",
+ ],
+
+ static_libs: [
+ "android.car.test.utils",
+ "android.car.watchdoglib",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "mockito-target-extended-minus-junit4",
+ "services.core",
+ "testng",
+ "truth-prebuilt",
+ "car-frameworks-service.impl",
+ ],
+
+ // mockito-target-extended dependencies
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ "libcarservicehelperjni",
+ ],
+}
diff --git a/builtInServices/tests/Android.mk b/builtInServices/tests/Android.mk
deleted file mode 100644
index c9eee58..0000000
--- a/builtInServices/tests/Android.mk
+++ /dev/null
@@ -1,47 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# We only want this apk build for tests.
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src) \
- $(call all-java-files-under, ../src) \
- $(call all-Iaidl-files-under, ../src)
-
-LOCAL_PACKAGE_NAME := FrameworkOptCarServicesTest
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_PRIVATE_PLATFORM_APIS := true
-
-LOCAL_CERTIFICATE := platform
-
-LOCAL_MODULE_TAGS := tests
-
-# When built explicitly put it in the data partition
-LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_JAVA_LIBRARIES += \
- android.car \
- android.test.runner \
- android.test.base \
- android.hardware.automotive.vehicle-V2.0-java \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
- android.car.test.utils \
- android.car.watchdoglib \
- androidx.test.ext.junit \
- androidx.test.rules \
- mockito-target-extended-minus-junit4 \
- services.core \
- testng \
- truth-prebuilt \
- android.car.builtin \
-
-# mockito-target-extended dependencies
-LOCAL_JNI_SHARED_LIBRARIES := \
- libdexmakerjvmtiagent \
- libstaticjvmtiagent \
-
-include $(BUILD_PACKAGE)
diff --git a/builtInServices/tests/res/raw/CSHS_classes.txt b/builtInServices/tests/res/raw/CSHS_classes.txt
new file mode 100644
index 0000000..d584c38
--- /dev/null
+++ b/builtInServices/tests/res/raw/CSHS_classes.txt
@@ -0,0 +1,16 @@
+com.android.internal.car.CarServiceHelperServiceUpdatable
+com.android.internal.car.CarServiceHelperInterface
+com.android.server.wm.ActivityInterceptorInfoWrapper
+com.android.server.wm.CarLaunchParamsModifierInterface
+com.android.server.wm.TaskWrapper
+com.android.server.wm.CarActivityInterceptorUpdatable
+com.android.server.wm.ActivityInterceptResultWrapper
+com.android.server.wm.WindowLayoutWrapper
+com.android.server.wm.CarLaunchParamsModifierUpdatable
+com.android.server.wm.TaskDisplayAreaWrapper
+com.android.server.wm.CalculateParams
+com.android.server.wm.LaunchParamsWrapper
+com.android.server.wm.ActivityOptionsWrapper
+com.android.server.wm.ActivityRecordWrapper
+com.android.server.wm.RequestWrapper
+com.android.server.wm.CarActivityInterceptorInterface
diff --git a/builtInServices/tests/src/com/android/internal/car/CarServiceHelperServiceTest.java b/builtInServices/tests/src/com/android/internal/car/CarServiceHelperServiceTest.java
index 552a27a..6ebc78e 100644
--- a/builtInServices/tests/src/com/android/internal/car/CarServiceHelperServiceTest.java
+++ b/builtInServices/tests/src/com/android/internal/car/CarServiceHelperServiceTest.java
@@ -16,36 +16,39 @@
package com.android.internal.car;
+import static com.android.car.internal.common.CommonConstants.INVALID_PID;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_POST_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;
-import static com.android.server.SystemService.UserCompletedEventType.newUserCompletedEventTypeForTest;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.SystemService.UserCompletedEventType.newUserCompletedEventTypeForTest;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
import android.car.watchdoglib.CarWatchdogDaemonHelper;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
+import android.os.ServiceDebugInfo;
import android.os.ServiceManager;
import android.os.UserHandle;
-import android.os.SystemProperties.Handle;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemService.TargetUser;
import com.android.server.SystemService.UserCompletedEventType;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.CarLaunchParamsModifier;
import org.junit.Before;
@@ -58,6 +61,8 @@ import org.mockito.Mock;
*/
@RunWith(AndroidJUnit4.class)
public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase {
+ private static final String SAMPLE_AIDL_VHAL_INTERFACE_NAME =
+ "android.hardware.automotive.vehicle.IVehicle/SampleVehicleHalService";
private CarServiceHelperService mHelper;
@@ -77,6 +82,12 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
@Mock
private CarDevicePolicySafetyChecker mCarDevicePolicySafetyChecker;
+ @Mock
+ private UserManagerInternal mUserManagerInternal;
+
+ @Mock
+ private ActivityManager mActivityManager;
+
public CarServiceHelperServiceTest() {
super(CarServiceHelperService.TAG);
}
@@ -86,7 +97,9 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
*/
@Override
protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
- session.spyStatic(ServiceManager.class);
+ session
+ .spyStatic(ServiceManager.class)
+ .spyStatic(LocalServices.class);
}
@Before
@@ -98,6 +111,23 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
mCarServiceHelperServiceUpdatable,
mCarDevicePolicySafetyChecker);
when(mMockContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mMockContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
+
+ doReturn(mUserManagerInternal)
+ .when(() -> LocalServices.getService(UserManagerInternal.class));
+ }
+
+ @Test
+ public void testIsUserSupported_preCreatedUserIsNotSupported() throws Exception {
+ expectWithMessage("isUserSupported")
+ .that(mHelper.isUserSupported(newTargetUser(10, /* preCreated= */ true)))
+ .isFalse();
+ }
+
+ @Test
+ public void testIsUserSupported_nonPreCreatedUserIsSupported() throws Exception {
+ expectWithMessage("isUserSupported").that(mHelper.isUserSupported(newTargetUser(11)))
+ .isTrue();
}
@Test
@@ -110,13 +140,6 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserStarting_preCreatedDoesntNotifyICar() throws Exception {
- mHelper.onUserStarting(newTargetUser(10, /* preCreated= */ true));
-
- verifyICarOnUserLifecycleEventNeverCalled();
- }
-
- @Test
public void testOnUserSwitching_notifiesICar() throws Exception {
int currentUserId = 10;
int targetUserId = 11;
@@ -129,13 +152,6 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserSwitching_preCreatedDoesntNotifyICar() throws Exception {
- mHelper.onUserSwitching(newTargetUser(10), newTargetUser(11, /* preCreated= */ true));
-
- verifyICarOnUserLifecycleEventNeverCalled();
- }
-
- @Test
public void testOnUserUnlocking_notifiesICar() throws Exception {
int userId = 10;
@@ -145,13 +161,6 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserUnlocking_preCreatedDoesntNotifyICar() throws Exception {
- mHelper.onUserUnlocking(newTargetUser(10, /* preCreated= */ true));
-
- verifyICarOnUserLifecycleEventNeverCalled();
- }
-
- @Test
public void testOnUserStopping_notifiesICar() throws Exception {
int userId = 10;
@@ -161,13 +170,6 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserStopping_preCreatedDoesntNotifyICar() throws Exception {
- mHelper.onUserStopping(newTargetUser(10, /* preCreated= */ true));
-
- verifyICarOnUserLifecycleEventNeverCalled();
- }
-
- @Test
public void testOnUserStopped_notifiesICar() throws Exception {
int userId = 10;
@@ -177,13 +179,6 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserStopped_preCreatedDoesntNotifyICar() throws Exception {
- mHelper.onUserStopped(newTargetUser(10, /* preCreated= */ true));
-
- verifyICarOnUserLifecycleEventNeverCalled();
- }
-
- @Test
public void testOnBootPhase_thirdPartyCanStart_initBootUser() throws Exception {
mHelper.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
@@ -201,16 +196,53 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
}
@Test
- public void testOnUserCompletedEvent_preCreatedUserDoesNotNotifyICar() throws Exception {
- UserCompletedEventType userCompletedEventType = newUserCompletedEventTypeForTest(
- UserCompletedEventType.EVENT_TYPE_USER_STARTING
- | UserCompletedEventType.EVENT_TYPE_USER_SWITCHING
- | UserCompletedEventType.EVENT_TYPE_USER_UNLOCKED);
+ public void testGetMainDisplayAssignedToUser() throws Exception {
+ when(mUserManagerInternal.getMainDisplayAssignedToUser(42)).thenReturn(108);
+
+ assertWithMessage("getMainDisplayAssignedToUser(42)")
+ .that(mHelper.getMainDisplayAssignedToUser(42)).isEqualTo(108);
+ }
+
+ @Test
+ public void testGetUserAssignedToDisplay() throws Exception {
+ when(mUserManagerInternal.getUserAssignedToDisplay(108)).thenReturn(42);
+
+ assertWithMessage("getUserAssignedToDisplay(108)")
+ .that(mHelper.getUserAssignedToDisplay(108)).isEqualTo(42);
+ }
+
+ @Test
+ public void testStartUserInBackgroundVisibleOnDisplay() throws Exception {
+ int userId = 100;
+ int displayId = 2;
- mHelper.onUserCompletedEvent(newTargetUser(10, /* preCreated= */true),
- userCompletedEventType);
+ mHelper.startUserInBackgroundVisibleOnDisplay(userId, displayId);
- verifyICarOnUserLifecycleEventNeverCalled();
+ verify(mActivityManager).startUserInBackgroundVisibleOnDisplay(userId, displayId);
+ }
+
+ @Test
+ public void testFetchAidlVhalPid() throws Exception {
+ int vhalPid = 5643;
+ ServiceDebugInfo[] debugInfos = {
+ newServiceDebugInfo(SAMPLE_AIDL_VHAL_INTERFACE_NAME, vhalPid),
+ newServiceDebugInfo("some.service", 1234),
+ };
+ doReturn(debugInfos).when(() -> ServiceManager.getServiceDebugInfo());
+
+ assertWithMessage("AIDL VHAL pid").that(mHelper.fetchAidlVhalPid()).isEqualTo(vhalPid);
+ }
+
+ @Test
+ public void testFetchAidlVhalPid_missingAidlVhalService() throws Exception {
+ ServiceDebugInfo[] debugInfos = {
+ newServiceDebugInfo("random.service", 8535),
+ newServiceDebugInfo("some.service", 1234),
+ };
+ doReturn(debugInfos).when(() -> ServiceManager.getServiceDebugInfo());
+
+ assertWithMessage("AIDL VHAL pid").that(mHelper.fetchAidlVhalPid())
+ .isEqualTo(INVALID_PID);
}
private TargetUser newTargetUser(int userId) {
@@ -249,12 +281,14 @@ public class CarServiceHelperServiceTest extends AbstractExtendedMockitoTestCase
null, UserHandle.of(userId));
}
- private void verifyICarOnUserLifecycleEventNeverCalled() throws Exception {
- verify(mCarServiceHelperServiceUpdatable, never()).sendUserLifecycleEvent(anyInt(), any(),
- any());
- }
-
private void verifyInitBootUser() throws Exception {
verify(mCarServiceHelperServiceUpdatable).initBootUser();
}
+
+ private ServiceDebugInfo newServiceDebugInfo(String name, int debugPid) {
+ ServiceDebugInfo serviceDebugInfo = new ServiceDebugInfo();
+ serviceDebugInfo.name = name;
+ serviceDebugInfo.debugPid = debugPid;
+ return serviceDebugInfo;
+ }
}
diff --git a/builtInServices/tests/src/com/android/server/wm/AnnotationTest.java b/builtInServices/tests/src/com/android/server/wm/AnnotationTest.java
new file mode 100644
index 0000000..4673f86
--- /dev/null
+++ b/builtInServices/tests/src/com/android/server/wm/AnnotationTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.car.test.util.AnnotationHelper.checkForAnnotation;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.annotation.AddedIn;
+import com.android.internal.car.R;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AnnotationTest {
+ @Test
+ public void testCarHelperServiceAPIAddedInAnnotation() throws Exception {
+ checkForAnnotation(readFile(R.raw.CSHS_classes), AddedIn.class);
+ }
+
+
+ private String[] readFile(int resourceId) throws IOException {
+ try (InputStream configurationStream = ApplicationProvider.getApplicationContext()
+ .getResources().openRawResource(resourceId)) {
+ return new String(configurationStream.readAllBytes()).split("\n");
+ }
+ }
+}
+
diff --git a/tools/OWNERS b/tools/OWNERS
new file mode 100644
index 0000000..e1e884a
--- /dev/null
+++ b/tools/OWNERS
@@ -0,0 +1 @@
+gargmayank@google.com
diff --git a/tools/repohookScript/annotation_classlist_repohook.py b/tools/repohookScript/annotation_classlist_repohook.py
new file mode 100755
index 0000000..d53624e
--- /dev/null
+++ b/tools/repohookScript/annotation_classlist_repohook.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+import os
+import subprocess
+import re
+
+from tempfile import NamedTemporaryFile
+from pathlib import Path
+
+# Helper method that strips out the parameter names of methods. This will allow users to change
+# parameter names for hidden apis without mistaking them as having been removed.
+# [^ ]* --> Negation set on SPACE character. This wll match everything until a SPACE.
+# *?(?=\)) --> This means the character ')' will not be included in the match.
+# [^ (]*?(?=\)) --> This will handle the last parameter at the end of a method signature.
+# It excludes matching any '(' characters when there are no parameters, i.e. method().
+# [^ ]*?(?=,) --> This will handle multiple parameters delimited by commas.
+def strip_param_names(api):
+ # get the arguments first
+ argGroup = re.search("\((.*)\)", api)
+ if argGroup is None:
+ return api
+ arg = argGroup.group(0)
+ new_arg = re.sub('[^ (]*?(?=\))|[^ ]*?(?=,)', "", arg)
+ return re.sub("\((.*)\)", new_arg, api)
+
+rootDir = os.getenv("ANDROID_BUILD_TOP")
+if rootDir is None or rootDir == "":
+ # env variable is not set. Then use the arg passed as Git root
+ rootDir = sys.argv[1]
+
+javaHomeDir = os.getenv("JAVA_HOME")
+if javaHomeDir is None or javaHomeDir == "":
+ if Path(rootDir + '/prebuilts/jdk/jdk17/linux-x86').is_dir():
+ javaHomeDir = rootDir + "/prebuilts/jdk/jdk17/linux-x86"
+ else:
+ print("$JAVA_HOME is not set. Please use source build/envsetup.sh` in $ANDROID_BUILD_TOP")
+ sys.exit(1)
+
+# Marker is set in GenerateApi.java class and should not be changed.
+marker = "Start-"
+options = ["--print-non-hidden-classes-CSHS",
+ "--print-addedin-without-requires-api-in-CSHS"]
+
+java_cmd = javaHomeDir + "/bin/java -jar " + rootDir + \
+ "/packages/services/Car/tools/GenericCarApiBuilder" \
+ "/GenericCarApiBuilder.jar --root-dir " + rootDir + " " + " ".join(options)
+
+all_data = subprocess.check_output(java_cmd, shell=True).decode('utf-8').strip().split("\n")
+all_results = []
+marker_index = []
+for i in range(len(all_data)):
+ if all_data[i].replace(marker, "") in options:
+ marker_index.append(i)
+
+previous_mark = 0
+for mark in marker_index:
+ if mark > previous_mark:
+ all_results.append(all_data[previous_mark+1:mark])
+ previous_mark = mark
+all_results.append(all_data[previous_mark+1:])
+
+# Update this line when adding more options
+new_class_list = all_results[0]
+incorrect_addedin_api_usage_in_CSHS_errors = all_results[1]
+
+existing_CSHS_classes_path = rootDir + "/frameworks/opt/car/services/builtInServices/tests/" \
+ "res/raw/CSHS_classes.txt"
+existing_class_list = []
+with open(existing_CSHS_classes_path) as f:
+ existing_class_list.extend(f.read().splitlines())
+
+# Find the diff in both class list
+extra_new_classes = [i for i in new_class_list if i not in existing_class_list]
+extra_deleted_classes = [i for i in existing_class_list if i not in new_class_list]
+
+# Print error is there is any class added or removed without changing test
+error = ""
+if len(extra_deleted_classes) > 0:
+ error = error + "Following Classes are deleted \n" + "\n".join(extra_deleted_classes)
+if len(extra_new_classes) > 0:
+ error = error + "\n\nFollowing new classes are added \n" + "\n".join(extra_new_classes)
+
+if error != "":
+ print(error)
+ print("\nRun following command to generate classlist for annotation test")
+ print("cd $ANDROID_BUILD_TOP && m -j GenericCarApiBuilder && GenericCarApiBuilder "
+ "--update-non-hidden-classes-CSHS")
+ print("\nThen run following test to make sure classes are properly annotated")
+ print("atest com.android.server.wm.AnnotationTest")
+ sys.exit(1)
+
+if len(incorrect_addedin_api_usage_in_CSHS_errors) > 0:
+ print("\nFollowing APIs are missing RequiresAPI annotations. See "
+ "go/car-api-version-annotation#using-requiresapi-for-version-check")
+ print("\n".join(incorrect_addedin_api_usage_in_CSHS_errors))
+ sys.exit(1)
diff --git a/updatableServices/Android.bp b/updatableServices/Android.bp
index 2adcba7..3069027 100644
--- a/updatableServices/Android.bp
+++ b/updatableServices/Android.bp
@@ -2,6 +2,16 @@ package {
default_applicable_licenses: ["Android-Apache-2.0"],
}
+filegroup {
+ name: "car-frameworks-updatable-service-sources",
+ srcs: [
+ "src/**/*.java",
+ ],
+ visibility: [
+ ":__subpackages__",
+ ],
+}
+
java_library {
name: "car-frameworks-service-module",
installable: true,
@@ -13,11 +23,11 @@ java_library {
"modules-utils-preconditions",
],
srcs: [
- "src/**/*.java",
+ ":car-frameworks-updatable-service-sources",
],
sdk_version: "module_current",
- min_sdk_version: "31",
+ min_sdk_version: "33",
apex_available: [
"//apex_available:platform",
"com.android.car.framework"
diff --git a/updatableServices/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImpl.java b/updatableServices/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImpl.java
index 0b3b094..5e6a746 100644
--- a/updatableServices/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImpl.java
+++ b/updatableServices/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImpl.java
@@ -15,9 +15,16 @@
*/
package com.android.internal.car.updatable;
+import static android.view.Display.INVALID_DISPLAY;
+
import static com.android.car.internal.SystemConstants.ICAR_SYSTEM_SERVER_CLIENT;
import static com.android.car.internal.common.CommonConstants.CAR_SERVICE_INTERFACE;
+import static com.android.car.internal.common.CommonConstants.INVALID_GID;
+import static com.android.car.internal.common.CommonConstants.INVALID_PID;
+import static com.android.car.internal.common.CommonConstants.INVALID_USER_ID;
+import static com.android.car.internal.util.VersionUtils.isPlatformVersionAtLeastU;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.ICar;
import android.car.ICarResultReceiver;
@@ -41,14 +48,17 @@ import com.android.car.internal.ICarServiceHelper;
import com.android.car.internal.ICarSystemServerClient;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.car.CarServiceHelperInterface;
import com.android.internal.car.CarServiceHelperServiceUpdatable;
-import java.io.File;
+import com.android.server.wm.CarActivityInterceptorInterface;
+import com.android.server.wm.CarActivityInterceptorUpdatableImpl;
import com.android.server.wm.CarLaunchParamsModifierInterface;
import com.android.server.wm.CarLaunchParamsModifierUpdatable;
import com.android.server.wm.CarLaunchParamsModifierUpdatableImpl;
+import java.io.File;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.Executor;
@@ -57,6 +67,7 @@ import java.util.function.BiConsumer;
/**
* Implementation of the abstract class CarServiceHelperUpdatable
*/
+@Keep
public final class CarServiceHelperServiceUpdatableImpl
implements CarServiceHelperServiceUpdatable, Executor {
@@ -85,7 +96,8 @@ public final class CarServiceHelperServiceUpdatableImpl
private final HandlerThread mHandlerThread = new HandlerThread(
CarServiceHelperServiceUpdatableImpl.class.getSimpleName());
- private final ICarServiceHelperImpl mHelper = new ICarServiceHelperImpl();
+ @VisibleForTesting
+ final ICarServiceHelperImpl mHelper = new ICarServiceHelperImpl();
private final CarServiceConnectedCallback mCarServiceConnectedCallback =
new CarServiceConnectedCallback();
@@ -95,18 +107,32 @@ public final class CarServiceHelperServiceUpdatableImpl
private final CarServiceHelperInterface mCarServiceHelperInterface;
private final CarLaunchParamsModifierUpdatableImpl mCarLaunchParamsModifierUpdatable;
+ private final CarActivityInterceptorUpdatableImpl mCarActivityInterceptorUpdatable;
+ /**
+ * This constructor is meant to be called using reflection by the builtin service and hence it
+ * shouldn't be changed as it is called from the platform with version {@link TIRAMISU}.
+ */
public CarServiceHelperServiceUpdatableImpl(Context context,
CarServiceHelperInterface carServiceHelperInterface,
CarLaunchParamsModifierInterface carLaunchParamsModifierInterface) {
this(context, carServiceHelperInterface, carLaunchParamsModifierInterface,
- /* carServiceProxy= */ null);
+ /* carActivityInterceptorInterface= */ null);
+ }
+
+ public CarServiceHelperServiceUpdatableImpl(Context context,
+ CarServiceHelperInterface carServiceHelperInterface,
+ CarLaunchParamsModifierInterface carLaunchParamsModifierInterface,
+ CarActivityInterceptorInterface carActivityInterceptorInterface) {
+ this(context, carServiceHelperInterface, carLaunchParamsModifierInterface,
+ carActivityInterceptorInterface, /* carServiceProxy= */ null);
}
@VisibleForTesting
CarServiceHelperServiceUpdatableImpl(Context context,
CarServiceHelperInterface carServiceHelperInterface,
CarLaunchParamsModifierInterface carLaunchParamsModifierInterface,
+ @Nullable CarActivityInterceptorInterface carActivityInterceptorInterface,
@Nullable CarServiceProxy carServiceProxy) {
mContext = context;
mHandlerThread.start();
@@ -114,6 +140,12 @@ public final class CarServiceHelperServiceUpdatableImpl
mCarServiceHelperInterface = carServiceHelperInterface;
mCarLaunchParamsModifierUpdatable = new CarLaunchParamsModifierUpdatableImpl(
carLaunchParamsModifierInterface);
+ if (isPlatformVersionAtLeastU()) {
+ mCarActivityInterceptorUpdatable = new CarActivityInterceptorUpdatableImpl(
+ (CarActivityInterceptorInterface) carActivityInterceptorInterface);
+ } else {
+ mCarActivityInterceptorUpdatable = null;
+ }
// carServiceProxy is Nullable because it is not possible to construct carServiceProxy with
// "this" object in the previous constructor as CarServiceHelperServiceUpdatableImpl has
// not been fully constructed.
@@ -176,6 +208,11 @@ public final class CarServiceHelperServiceUpdatableImpl
return mCarLaunchParamsModifierUpdatable;
}
+ @Override
+ public CarActivityInterceptorUpdatableImpl getCarActivityInterceptorUpdatable() {
+ return mCarActivityInterceptorUpdatable;
+ }
+
@VisibleForTesting
void handleCarServiceConnection(IBinder iBinder) {
synchronized (mLock) {
@@ -274,7 +311,8 @@ public final class CarServiceHelperServiceUpdatableImpl
mCarServiceProxy.dump(new IndentingPrintWriter(writer));
}
- private final class ICarServiceHelperImpl extends ICarServiceHelper.Stub {
+ @VisibleForTesting
+ final class ICarServiceHelperImpl extends ICarServiceHelper.Stub {
@Override
public void setDisplayAllowlistForUser(int userId, int[] displayIds) {
@@ -300,6 +338,13 @@ public final class CarServiceHelperServiceUpdatableImpl
}
@Override
+ public void setPersistentActivitiesOnRootTask(@NonNull List<ComponentName> activities,
+ IBinder rootTaskToken) {
+ mCarActivityInterceptorUpdatable.setPersistentActivityOnRootTask(activities,
+ rootTaskToken);
+ }
+
+ @Override
public void setSafetyMode(boolean safe) {
mCarServiceHelperInterface.setSafetyMode(safe);
}
@@ -313,6 +358,63 @@ public final class CarServiceHelperServiceUpdatableImpl
public void sendInitialUser(UserHandle user) {
mCarServiceProxy.saveInitialUser(user);
}
+
+ @Override
+ public void setProcessGroup(int pid, int group) {
+ if (!isPlatformVersionAtLeastU()) {
+ return;
+ }
+ mCarServiceHelperInterface.setProcessGroup(pid, group);
+ }
+
+ @Override
+ public int getProcessGroup(int pid) {
+ if (isPlatformVersionAtLeastU()) {
+ return mCarServiceHelperInterface.getProcessGroup(pid);
+ }
+ return INVALID_GID;
+ }
+
+ @Override
+ public int getMainDisplayAssignedToUser(int userId) {
+ if (isPlatformVersionAtLeastU()) {
+ return mCarServiceHelperInterface.getMainDisplayAssignedToUser(userId);
+ }
+ return INVALID_DISPLAY;
+ }
+
+ @Override
+ public int getUserAssignedToDisplay(int displayId) {
+ if (isPlatformVersionAtLeastU()) {
+ return mCarServiceHelperInterface.getUserAssignedToDisplay(displayId);
+ }
+ return INVALID_USER_ID;
+ }
+
+ @Override
+ public boolean startUserInBackgroundVisibleOnDisplay(int userId, int displayId) {
+ if (isPlatformVersionAtLeastU()) {
+ return mCarServiceHelperInterface.startUserInBackgroundVisibleOnDisplay(
+ userId, displayId);
+ }
+ return false;
+ }
+
+ @Override
+ public void setProcessProfile(int pid, int uid, @NonNull String profile) {
+ if (!isPlatformVersionAtLeastU()) {
+ return;
+ }
+ mCarServiceHelperInterface.setProcessProfile(pid, uid, profile);
+ }
+
+ @Override
+ public int fetchAidlVhalPid() {
+ if (isPlatformVersionAtLeastU()) {
+ return mCarServiceHelperInterface.fetchAidlVhalPid();
+ }
+ return INVALID_PID;
+ }
}
private final class CarServiceConnectedCallback extends ICarResultReceiver.Stub {
diff --git a/updatableServices/src/com/android/internal/car/updatable/CarServiceProxy.java b/updatableServices/src/com/android/internal/car/updatable/CarServiceProxy.java
index 0824755..4f830cb 100644
--- a/updatableServices/src/com/android/internal/car/updatable/CarServiceProxy.java
+++ b/updatableServices/src/com/android/internal/car/updatable/CarServiceProxy.java
@@ -16,12 +16,16 @@
package com.android.internal.car.updatable;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_CREATED;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_INVISIBLE;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_REMOVED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_STOPPING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_UNLOCKING;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_VISIBLE;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -197,7 +201,7 @@ final class CarServiceProxy {
boolean user0IsCurrent = lastSwitchedUser == USER_SYSTEM;
// If user0Lifecycle is 0, then no life-cycle event received yet.
if (user0Lifecycle != 0) {
- sendAllLifecyleToUser(USER_SYSTEM, user0Lifecycle,
+ sendAllLifecycleToUser(USER_SYSTEM, user0Lifecycle,
user0IsCurrent);
}
lastUserLifecycle.delete(USER_SYSTEM);
@@ -207,7 +211,7 @@ final class CarServiceProxy {
int currentUserLifecycle = lastUserLifecycle.get(lastSwitchedUser);
// If currentUserLifecycle is 0, then no life-cycle event received yet.
if (currentUserLifecycle != 0) {
- sendAllLifecyleToUser(lastSwitchedUser, currentUserLifecycle,
+ sendAllLifecycleToUser(lastSwitchedUser, currentUserLifecycle,
/* isCurrentUser= */ true);
}
}
@@ -218,15 +222,56 @@ final class CarServiceProxy {
for (int i = 0; i < lastUserLifecycle.size(); i++) {
int userId = lastUserLifecycle.keyAt(i);
int lifecycle = lastUserLifecycle.valueAt(i);
- sendAllLifecyleToUser(userId, lifecycle, /* isCurrentUser= */ false);
+ sendAllLifecycleToUser(userId, lifecycle, /* isCurrentUser= */ false);
}
}
- private void sendAllLifecyleToUser(@UserIdInt int userId, int lifecycle,
+ private void sendAllLifecycleToUser(@UserIdInt int userId, int lifecycle,
boolean isCurrentUser) {
if (DBG) {
- Slogf.d(TAG, "sendAllLifecyleToUser, user:" + userId + " lifecycle:" + lifecycle);
+ Slogf.d(TAG, "sendAllLifecycleToUser, user:" + userId + " lifecycle:" + lifecycle);
}
+
+ // User created and user removed are unrelated to the user switching/unlocking flow.
+ // Return early to prevent them from going into the following logic
+ // that makes assumptions about the sequence of lifecycle event types
+ // following numerical order.
+ if (lifecycle == USER_LIFECYCLE_EVENT_TYPE_CREATED) {
+ sendUserLifecycleEventInternal(USER_LIFECYCLE_EVENT_TYPE_CREATED,
+ UserManagerHelper.USER_NULL, userId);
+ return;
+ }
+
+ if (lifecycle == USER_LIFECYCLE_EVENT_TYPE_REMOVED) {
+ sendUserLifecycleEventInternal(USER_LIFECYCLE_EVENT_TYPE_REMOVED,
+ UserManagerHelper.USER_NULL, userId);
+ return;
+ }
+
+ // User visible and user invisible are unrelated to the user switching/unlocking flow.
+ // Return early to prevent them from going into the following logic
+ // that makes assumptions about the sequence of lifecycle event types
+ // following numerical order.
+ // If we don't return early here, because the user visible and visible event numbers are
+ // greater than user starting/switching/unlocking/unlocked events, they will cause these
+ // events to be sent which is an unintended effect.
+ // TODO(b/277148129): Refactor the entire lifecycle events replay logic taking into
+ // consideration the visible and invisible events. Currently only the last event per use is
+ // tracked so it's hard to infer events before user visible and user invisible.
+ if (lifecycle == USER_LIFECYCLE_EVENT_TYPE_VISIBLE) {
+ sendUserLifecycleEventInternal(USER_LIFECYCLE_EVENT_TYPE_VISIBLE,
+ UserManagerHelper.USER_NULL, userId);
+ return;
+ }
+
+ if (lifecycle == USER_LIFECYCLE_EVENT_TYPE_INVISIBLE) {
+ sendUserLifecycleEventInternal(USER_LIFECYCLE_EVENT_TYPE_INVISIBLE,
+ UserManagerHelper.USER_NULL, userId);
+ return;
+ }
+
+ // The following logic makes assumptions about the sequence of lifecycle event types
+ // following numerical order.
if (lifecycle >= USER_LIFECYCLE_EVENT_TYPE_STARTING) {
sendUserLifecycleEventInternal(USER_LIFECYCLE_EVENT_TYPE_STARTING,
UserManagerHelper.USER_NULL, userId);
@@ -401,8 +446,11 @@ final class CarServiceProxy {
Preconditions.checkArgument((value instanceof UserHandle),
"Invalid value for ON_USER_REMOVED: %s", value);
UserHandle user = (UserHandle) value;
+ // TODO(235524989): Consolidating logging with other lifecycle events,
+ // including user metrics.
if (DBG) Slogf.d(TAG, "Sending onUserRemoved(): " + user);
- mCarService.onUserRemoved(user);
+ mCarService.onUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_REMOVED,
+ UserManagerHelper.USER_NULL, user.getIdentifier());
}
/**
diff --git a/updatableServices/src/com/android/server/wm/CarActivityInterceptorUpdatableImpl.java b/updatableServices/src/com/android/server/wm/CarActivityInterceptorUpdatableImpl.java
new file mode 100644
index 0000000..ee0d999
--- /dev/null
+++ b/updatableServices/src/com/android/server/wm/CarActivityInterceptorUpdatableImpl.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.annotation.SystemApi;
+import android.app.ActivityOptions;
+import android.car.builtin.util.Slogf;
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Implementation of {@link CarActivityInterceptorUpdatable}.
+ *
+ * @hide
+ */
+@RequiresApi(UPSIDE_DOWN_CAKE)
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public final class CarActivityInterceptorUpdatableImpl implements CarActivityInterceptorUpdatable {
+ public static final String TAG = CarActivityInterceptorUpdatableImpl.class.getSimpleName();
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final ArrayMap<ComponentName, IBinder> mActivityToRootTaskMap = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final Set<IBinder> mKnownRootTasks = new ArraySet<>();
+ private final CarActivityInterceptorInterface mBuiltIn;
+
+ public CarActivityInterceptorUpdatableImpl(CarActivityInterceptorInterface builtInInterface) {
+ mBuiltIn = builtInInterface;
+ }
+
+ @Override
+ public ActivityInterceptResultWrapper onInterceptActivityLaunch(
+ ActivityInterceptorInfoWrapper info) {
+ if (info.getIntent() == null) {
+ return null;
+ }
+ ComponentName componentName = info.getIntent().getComponent();
+
+ synchronized (mLock) {
+ int keyIndex = mActivityToRootTaskMap.indexOfKey(componentName);
+ if (keyIndex >= 0) {
+ IBinder rootTaskToken = mActivityToRootTaskMap.valueAt(keyIndex);
+ if (!isRootTaskUserSameAsActivityUser(rootTaskToken, info)) {
+ return null;
+ }
+
+ ActivityOptionsWrapper optionsWrapper = info.getCheckedOptions();
+ if (optionsWrapper == null) {
+ optionsWrapper = ActivityOptionsWrapper.create(ActivityOptions.makeBasic());
+ }
+ optionsWrapper.setLaunchRootTask(rootTaskToken);
+ return ActivityInterceptResultWrapper.create(info.getIntent(),
+ optionsWrapper.getOptions());
+ }
+ }
+ return null;
+ }
+
+ private boolean isRootTaskUserSameAsActivityUser(IBinder rootTaskToken,
+ ActivityInterceptorInfoWrapper activityInterceptorInfoWrapper) {
+ TaskWrapper rootTask = TaskWrapper.createFromToken(rootTaskToken);
+ int userIdFromActivity = activityInterceptorInfoWrapper.getUserId();
+ int userIdFromRootTask = mBuiltIn.getUserAssignedToDisplay(rootTask
+ .getTaskDisplayArea().getDisplay().getDisplayId());
+ if (userIdFromActivity == userIdFromRootTask) {
+ return true;
+ }
+ Slogf.w(TAG, "The user id of launched activity (%d) doesn't match the "
+ + "user id which the display (which the root task is added in) is "
+ + "assigned to (%d).", userIdFromActivity, userIdFromRootTask);
+ return false;
+ }
+
+ /**
+ * Sets the given {@code activities} to be persistent on the root task corresponding to the
+ * given {@code rootTaskToken}.
+ * <p>
+ * If {@code rootTaskToken} is {@code null}, then the earlier root task associations of the
+ * given {@code activities} will be removed.
+ *
+ * @param activities the list of activities which have to be persisted.
+ * @param rootTaskToken the binder token of the root task which the activities have to be
+ * persisted on.
+ */
+ public void setPersistentActivityOnRootTask(@NonNull List<ComponentName> activities,
+ IBinder rootTaskToken) {
+ synchronized (mLock) {
+ if (rootTaskToken == null) {
+ int activitiesNum = activities.size();
+ for (int i = 0; i < activitiesNum; i++) {
+ mActivityToRootTaskMap.remove(activities.get(i));
+ }
+ return;
+ }
+
+ int activitiesNum = activities.size();
+ for (int i = 0; i < activitiesNum; i++) {
+ mActivityToRootTaskMap.put(activities.get(i), rootTaskToken);
+ }
+ if (!mKnownRootTasks.contains(rootTaskToken)) {
+ // Seeing the token for the first time, set the listener
+ removeRootTaskTokenOnDeath(rootTaskToken);
+ mKnownRootTasks.add(rootTaskToken);
+ }
+ }
+ }
+
+ private void removeRootTaskTokenOnDeath(IBinder rootTaskToken) {
+ try {
+ rootTaskToken.linkToDeath(() -> removeRootTaskToken(rootTaskToken), /* flags= */ 0);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void removeRootTaskToken(IBinder rootTaskToken) {
+ synchronized (mLock) {
+ mKnownRootTasks.remove(rootTaskToken);
+ // remove all the persistent activities for this root task token from the map,
+ // because the root task itself is removed.
+ Iterator<Map.Entry<ComponentName, IBinder>> iterator =
+ mActivityToRootTaskMap.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<ComponentName, IBinder> entry = iterator.next();
+ if (entry.getValue().equals(rootTaskToken)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public Map<ComponentName, IBinder> getActivityToRootTaskMap() {
+ synchronized (mLock) {
+ return mActivityToRootTaskMap;
+ }
+ }
+}
diff --git a/updatableServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatableImpl.java b/updatableServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatableImpl.java
index d836c03..a5e79b9 100644
--- a/updatableServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatableImpl.java
+++ b/updatableServices/src/com/android/server/wm/CarLaunchParamsModifierUpdatableImpl.java
@@ -16,10 +16,13 @@
package com.android.server.wm;
+import static android.car.PlatformVersion.VERSION_CODES;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.UserIdInt;
+import android.car.PlatformVersionMismatchException;
import android.car.app.CarActivityManager;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
@@ -30,9 +33,11 @@ import android.hardware.display.DisplayManager;
import android.os.ServiceSpecificException;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseIntArray;
import android.view.Display;
+import com.android.car.internal.util.VersionUtils;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
@@ -50,6 +55,8 @@ public final class CarLaunchParamsModifierUpdatableImpl
implements CarLaunchParamsModifierUpdatable {
private static final String TAG = "CAR.LAUNCH";
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ // Comes from android.os.UserHandle.USER_NULL.
+ private static final int USER_NULL = -10000;
private final CarLaunchParamsModifierInterface mBuiltin;
private final Object mLock = new Object();
@@ -57,7 +64,7 @@ public final class CarLaunchParamsModifierUpdatableImpl
// Always start with USER_SYSTEM as the timing of handleCurrentUserSwitching(USER_SYSTEM) is not
// guaranteed to be earler than 1st Activity launch.
@GuardedBy("mLock")
- private int mCurrentDriverUser = UserManagerHelper.USER_SYSTEM;
+ private int mDriverUser = UserManagerHelper.USER_SYSTEM;
// TODO: Switch from tracking displays to tracking display areas instead
/**
@@ -145,32 +152,61 @@ public final class CarLaunchParamsModifierUpdatableImpl
}
}
- /** Notifies user starting. */
- public void handleUserStarting(int startingUser) {
- // Do nothing
+ @Override
+ public void handleUserVisibilityChanged(int userId, boolean visible) {
+ synchronized (mLock) {
+ if (DBG) {
+ Slogf.d(TAG, "handleUserVisibilityChanged user=%d, visible=%b",
+ userId, visible);
+ }
+ if (userId != mDriverUser || visible) {
+ return;
+ }
+ int currentOrTargetUserId = getCurrentOrTargetUserId();
+ maySwitchCurrentDriver(currentOrTargetUserId);
+ }
+ }
+
+ private int getCurrentOrTargetUserId() {
+ if (!VersionUtils.isPlatformVersionAtLeastU()) {
+ throw new PlatformVersionMismatchException(VERSION_CODES.UPSIDE_DOWN_CAKE_0);
+ }
+ Pair<Integer, Integer> currentAndTargetUserIds = mBuiltin.getCurrentAndTargetUserIds();
+ int currentUserId = currentAndTargetUserIds.first;
+ int targetUserId = currentAndTargetUserIds.second;
+ int currentOrTargetUserId = targetUserId != USER_NULL ? targetUserId : currentUserId;
+ return currentOrTargetUserId;
}
/** Notifies user switching. */
public void handleCurrentUserSwitching(@UserIdInt int newUserId) {
+ if (DBG) Slogf.d(TAG, "handleCurrentUserSwitching user=%d", newUserId);
+ maySwitchCurrentDriver(newUserId);
+ }
+
+ private void maySwitchCurrentDriver(int userId) {
synchronized (mLock) {
- mCurrentDriverUser = newUserId;
+ if (DBG) {
+ Slogf.d(TAG, "maySwitchCurrentDriver old=%d, new=%d", mDriverUser, userId);
+ }
+ if (mDriverUser == userId) {
+ return;
+ }
+ mDriverUser = userId;
mDefaultDisplayForProfileUser.clear();
mDisplayToProfileUserMapping.clear();
}
}
- @GuardedBy("mLock")
- private void removeUserFromAllowlistsLocked(int userId) {
- for (int i = mDisplayToProfileUserMapping.size() - 1; i >= 0; i--) {
- if (mDisplayToProfileUserMapping.valueAt(i) == userId) {
- mDisplayToProfileUserMapping.removeAt(i);
- }
- }
- mDefaultDisplayForProfileUser.delete(userId);
+ /** Notifies user starting. */
+ public void handleUserStarting(int startingUser) {
+ if (DBG) Slogf.d(TAG, "handleUserStarting user=%d", startingUser);
+ // Do nothing
}
/** Notifies user stopped. */
public void handleUserStopped(@UserIdInt int stoppedUser) {
+ if (DBG) Slogf.d(TAG, "handleUserStopped user=%d", stoppedUser);
// Note that the current user is never stopped. It always takes switching into
// non-current user before stopping the user.
synchronized (mLock) {
@@ -178,6 +214,16 @@ public final class CarLaunchParamsModifierUpdatableImpl
}
}
+ @GuardedBy("mLock")
+ private void removeUserFromAllowlistsLocked(int userId) {
+ for (int i = mDisplayToProfileUserMapping.size() - 1; i >= 0; i--) {
+ if (mDisplayToProfileUserMapping.valueAt(i) == userId) {
+ mDisplayToProfileUserMapping.removeAt(i);
+ }
+ }
+ mDefaultDisplayForProfileUser.delete(userId);
+ }
+
/**
* Sets display allowlist for the {@code userId}. For passenger user, activity will be always
* launched to a display in the allowlist. If requested display is not in the allowlist, the 1st
@@ -198,7 +244,7 @@ public final class CarLaunchParamsModifierUpdatableImpl
+ " not in passenger display list:%s", displayId, mPassengerDisplays);
continue;
}
- if (userId == mCurrentDriverUser) {
+ if (userId == mDriverUser) {
mDisplayToProfileUserMapping.delete(displayId);
} else {
mDisplayToProfileUserMapping.put(displayId, userId);
@@ -272,11 +318,11 @@ public final class CarLaunchParamsModifierUpdatableImpl
TaskDisplayAreaWrapper originalDisplayArea = currentParams.getPreferredTaskDisplayArea();
// DisplayArea where CarLaunchParamsModifier targets to launch the Activity.
TaskDisplayAreaWrapper targetDisplayArea = null;
+ ComponentName activityName = activity.getComponentName();
if (DBG) {
- Slogf.d(TAG, "onCalculate, userId:%d original displayArea:%s ActivityOptions:%s",
- userId, originalDisplayArea, options);
+ Slogf.d(TAG, "onCalculate, userId:%d original displayArea:%s actvity:%s options:%s",
+ userId, originalDisplayArea, activityName, options);
}
- ComponentName activityName = activity.getComponentName();
decision:
synchronized (mLock) {
// If originalDisplayArea is set, respect that before ActivityOptions check.
@@ -307,19 +353,22 @@ public final class CarLaunchParamsModifierUpdatableImpl
targetDisplayArea = mBuiltin.getDefaultTaskDisplayAreaOnDisplay(
Display.DEFAULT_DISPLAY);
}
- if (userId == mCurrentDriverUser) {
+ if (userId == mDriverUser) {
// Respect the existing DisplayArea.
+ if (DBG) Slogf.d(TAG, "Skip the further check for Driver");
break decision;
}
if (userId == UserManagerHelper.USER_SYSTEM) {
// This will be only allowed if it has FLAG_SHOW_FOR_ALL_USERS.
// The flag is not immediately accessible here so skip the check.
// But other WM policy will enforce it.
+ if (DBG) Slogf.d(TAG, "Skip the further check for SystemUser");
break decision;
}
// Now user is a passenger.
if (mPassengerDisplays.isEmpty()) {
// No displays for passengers. This could be old user and do not do anything.
+ if (DBG) Slogf.d(TAG, "Skip the further check for no PassengerDisplays");
break decision;
}
if (targetDisplayArea == null) {
@@ -333,16 +382,18 @@ public final class CarLaunchParamsModifierUpdatableImpl
Display display = targetDisplayArea.getDisplay();
if ((display.getFlags() & Display.FLAG_PRIVATE) != 0) {
// private display should follow its own restriction rule.
+ if (DBG) Slogf.d(TAG, "Skip the further check for the private display");
break decision;
}
if (DisplayHelper.getType(display) == DisplayHelper.TYPE_VIRTUAL) {
// TODO(b/132903422) : We need to update this after the bug is resolved.
// For now, don't change anything.
+ if (DBG) Slogf.d(TAG, "Skip the further check for the virtual display");
break decision;
}
- int userForDisplay = mDisplayToProfileUserMapping.get(display.getDisplayId(),
- UserManagerHelper.USER_NULL);
+ int userForDisplay = getUserForDisplayLocked(display.getDisplayId());
if (userForDisplay == userId) {
+ if (DBG) Slogf.d(TAG, "The display is assigned for the user");
break decision;
}
targetDisplayArea = getAlternativeDisplayAreaForPassengerLocked(
@@ -350,8 +401,14 @@ public final class CarLaunchParamsModifierUpdatableImpl
}
if (targetDisplayArea != null && originalDisplayArea != targetDisplayArea) {
Slogf.i(TAG, "Changed launching display, user:%d requested display area:%s"
- + " target display area:", userId, originalDisplayArea, targetDisplayArea);
+ + " target display area:%s", userId, originalDisplayArea, targetDisplayArea);
outParams.setPreferredTaskDisplayArea(targetDisplayArea);
+ if (VersionUtils.isPlatformVersionAtLeastU()
+ && options != null
+ && options.getLaunchWindowingMode()
+ != ActivityOptionsWrapper.WINDOWING_MODE_UNDEFINED) {
+ outParams.setWindowingMode(options.getLaunchWindowingMode());
+ }
return LaunchParamsWrapper.RESULT_DONE;
} else {
return LaunchParamsWrapper.RESULT_SKIP;
@@ -359,9 +416,23 @@ public final class CarLaunchParamsModifierUpdatableImpl
}
@GuardedBy("mLock")
+ private int getUserForDisplayLocked(int displayId) {
+ int userForDisplay = mDisplayToProfileUserMapping.get(displayId,
+ UserManagerHelper.USER_NULL);
+ if (userForDisplay != UserManagerHelper.USER_NULL) {
+ return userForDisplay;
+ }
+ if (VersionUtils.isPlatformVersionAtLeastU()) {
+ userForDisplay = mBuiltin.getUserAssignedToDisplay(displayId);
+ }
+ return userForDisplay;
+ }
+
+ @GuardedBy("mLock")
@Nullable
private TaskDisplayAreaWrapper getAlternativeDisplayAreaForPassengerLocked(int userId,
@NonNull ActivityRecordWrapper activtyRecord, @Nullable RequestWrapper request) {
+ if (DBG) Slogf.d(TAG, "getAlternativeDisplayAreaForPassengerLocked:%d", userId);
List<TaskDisplayAreaWrapper> fallbacks = mBuiltin.getFallbackDisplayAreasForActivity(
activtyRecord, request);
for (int i = 0, size = fallbacks.size(); i < size; ++i) {
@@ -401,8 +472,18 @@ public final class CarLaunchParamsModifierUpdatableImpl
int displayId = mDefaultDisplayForProfileUser.get(userId);
return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
}
+ if (VersionUtils.isPlatformVersionAtLeastU()) {
+ int displayId = mBuiltin.getMainDisplayAssignedToUser(userId);
+ if (displayId != Display.INVALID_DISPLAY) {
+ return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
+ }
+ }
if (!mPassengerDisplays.isEmpty()) {
int displayId = mPassengerDisplays.get(0);
+ if (DBG) {
+ Slogf.d(TAG, "fallbackDisplayAreaForUserLocked: userId=%d, displayId=%d",
+ userId, displayId);
+ }
return mBuiltin.getDefaultTaskDisplayAreaOnDisplay(displayId);
}
return null;
@@ -437,4 +518,4 @@ public final class CarLaunchParamsModifierUpdatableImpl
}
return CarActivityManager.RESULT_SUCCESS;
}
-} \ No newline at end of file
+}
diff --git a/updatableServices/tests/Android.bp b/updatableServices/tests/Android.bp
index 7d9e971..c01ffeb 100644
--- a/updatableServices/tests/Android.bp
+++ b/updatableServices/tests/Android.bp
@@ -10,6 +10,7 @@ android_test {
srcs: [
"src/**/*.java",
+ ":car-frameworks-updatable-service-sources",
],
platform_apis: true,
@@ -34,7 +35,6 @@ android_test {
"androidx.test.ext.junit",
"androidx.test.rules",
"car-frameworks-service.impl",
- "car-frameworks-service-module",
"mockito-target-extended-minus-junit4",
"services.core",
"testng",
diff --git a/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImplTest.java b/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImplTest.java
index 6e5231b..d608e12 100644
--- a/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImplTest.java
+++ b/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceHelperServiceUpdatableImplTest.java
@@ -20,6 +20,8 @@ import static com.android.car.internal.common.CommonConstants.CAR_SERVICE_INTERF
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -36,15 +38,18 @@ import android.os.UserHandle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.car.internal.util.VersionUtils;
import com.android.internal.car.CarServiceHelperInterface;
+import com.android.server.wm.CarActivityInterceptorInterface;
import com.android.server.wm.CarLaunchParamsModifierInterface;
-import java.util.function.BiConsumer;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.MockitoSession;
+
+import java.util.function.BiConsumer;
/**
* This class contains unit tests for the {@link CarServiceHelperServiceUpdatableImpl}.
@@ -53,6 +58,8 @@ import org.mockito.Mock;
public final class CarServiceHelperServiceUpdatableImplTest
extends AbstractExtendedMockitoTestCase {
+ private MockitoSession mSession;
+
@Mock
private Context mMockContext;
@Mock
@@ -62,6 +69,8 @@ public final class CarServiceHelperServiceUpdatableImplTest
@Mock
private CarLaunchParamsModifierInterface mCarLaunchParamsModifierInterface;
@Mock
+ private CarActivityInterceptorInterface mCarActivityInterceptorInterface;
+ @Mock
private ICar mICarBinder;
@Mock
private IBinder mIBinder;
@@ -78,9 +87,16 @@ public final class CarServiceHelperServiceUpdatableImplTest
mMockContext,
mCarServiceHelperInterface,
mCarLaunchParamsModifierInterface,
+ mCarActivityInterceptorInterface,
mCarServiceProxy);
}
+ @Override
+ protected void onSessionBuilder(
+ AbstractExtendedMockitoTestCase.CustomMockitoSessionBuilder builder) {
+ builder.spyStatic(VersionUtils.class);
+ }
+
@Test
public void testCarServiceLaunched() throws Exception {
mockSystemContext();
@@ -158,6 +174,52 @@ public final class CarServiceHelperServiceUpdatableImplTest
userTo.getIdentifier());
}
+ @Test
+ public void testGetProcessGroup() throws Exception {
+ when(mCarServiceHelperInterface.getProcessGroup(42)).thenReturn(108);
+
+ assertWithMessage("getProcessGroup(42)")
+ .that(mCarServiceHelperServiceUpdatableImpl.mHelper.getProcessGroup(42))
+ .isEqualTo(108);
+ }
+
+ @Test
+ public void testSetProcessGroup() throws Exception {
+ mCarServiceHelperServiceUpdatableImpl.mHelper.setProcessGroup(42, 108);
+
+ verify(mCarServiceHelperInterface).setProcessGroup(42, 108);
+ }
+
+ @Test
+ public void testStartUserInBackgroundVisibleOnDisplay() throws Exception {
+ int userId = 100;
+ int displayId = 2;
+
+ mCarServiceHelperServiceUpdatableImpl.mHelper.startUserInBackgroundVisibleOnDisplay(userId,
+ displayId);
+
+ verify(mCarServiceHelperInterface).startUserInBackgroundVisibleOnDisplay(userId,
+ displayId);
+ }
+
+ @Test
+ public void testGetMainDisplayAssignedToUser() throws Exception {
+ when(mCarServiceHelperInterface.getMainDisplayAssignedToUser(42)).thenReturn(108);
+ assertWithMessage("getMainDisplayAssignedToUser(42)")
+ .that(mCarServiceHelperServiceUpdatableImpl.mHelper
+ .getMainDisplayAssignedToUser(42))
+ .isEqualTo(108);
+ }
+
+ @Test
+ public void testGetUserAssignedToDisplay() throws Exception {
+ when(mCarServiceHelperInterface.getUserAssignedToDisplay(42)).thenReturn(108);
+
+ assertWithMessage("getUserAssignedToDisplay(42)")
+ .that(mCarServiceHelperServiceUpdatableImpl.mHelper.getUserAssignedToDisplay(42))
+ .isEqualTo(108);
+ }
+
private void mockICarBinder() {
when(ICar.Stub.asInterface(mIBinder)).thenReturn(mICarBinder);
}
diff --git a/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceProxyTest.java b/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceProxyTest.java
index 714cf55..39781c5 100644
--- a/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceProxyTest.java
+++ b/updatableServices/tests/src/com/android/internal/car/updatable/CarServiceProxyTest.java
@@ -15,6 +15,7 @@
*/
package com.android.internal.car.updatable;
+import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_REMOVED;
import static com.android.car.internal.common.CommonConstants.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static org.mockito.Mockito.any;
@@ -27,6 +28,7 @@ import android.car.test.mocks.AbstractExtendedMockitoTestCase;
import android.car.test.util.UserTestingHelper.UserInfoBuilder;
import android.content.pm.UserInfo;
import android.os.RemoteException;
+import android.os.UserHandle;
import com.android.car.internal.ICarSystemServerClient;
import com.android.server.SystemService.TargetUser;
@@ -115,7 +117,7 @@ public class CarServiceProxyTest extends AbstractExtendedMockitoTestCase {
verifyInitBootUserCalled();
verifySendLifecycleEventCalled(USER_LIFECYCLE_EVENT_TYPE_SWITCHING);
- verifyOnUserRemovedCalled();
+ verifyLifecycleEventCalledForUserRemoval();
}
@Test
@@ -124,14 +126,14 @@ public class CarServiceProxyTest extends AbstractExtendedMockitoTestCase {
callOnUserRemoved();
- verifyOnUserRemovedCalled();
+ verifyLifecycleEventCalledForUserRemoval();
}
@Test
public void testOnUserRemoved_CarServiceNull() throws RemoteException {
callOnUserRemoved();
- verifyOnUserRemovedNeverCalled();
+ verifySendLifecycleEventNeverCalled();
}
@Test
@@ -203,10 +205,13 @@ public class CarServiceProxyTest extends AbstractExtendedMockitoTestCase {
verify(mCarService, never()).onUserLifecycleEvent(anyInt(), anyInt(), anyInt());
}
- private void verifyOnUserRemovedCalled() throws RemoteException {
- verify(mCarService).onUserRemoved(mRemovedUser1.getUserHandle());
- verify(mCarService).onUserRemoved(mRemovedUser2.getUserHandle());
- verify(mCarService).onUserRemoved(mRemovedUser3.getUserHandle());
+ private void verifyLifecycleEventCalledForUserRemoval() throws RemoteException {
+ verify(mCarService).onUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_REMOVED,
+ UserHandle.USER_NULL, mRemovedUser1.getUserHandle().getIdentifier());
+ verify(mCarService).onUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_REMOVED,
+ UserHandle.USER_NULL, mRemovedUser2.getUserHandle().getIdentifier());
+ verify(mCarService).onUserLifecycleEvent(USER_LIFECYCLE_EVENT_TYPE_REMOVED,
+ UserHandle.USER_NULL, mRemovedUser3.getUserHandle().getIdentifier());
}
private void verifyOnUserRemovedNeverCalled() throws RemoteException {
diff --git a/updatableServices/tests/src/com/android/server/wm/CarActivityInterceptorUpdatableTest.java b/updatableServices/tests/src/com/android/server/wm/CarActivityInterceptorUpdatableTest.java
new file mode 100644
index 0000000..339616b
--- /dev/null
+++ b/updatableServices/tests/src/com/android/server/wm/CarActivityInterceptorUpdatableTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.view.Display;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class CarActivityInterceptorUpdatableTest {
+ private static final int DEFAULT_CURRENT_USER_ID = 112;
+ private static final int PASSENGER_USER_ID = 198;
+ private CarActivityInterceptorUpdatableImpl mInterceptor;
+ private MockitoSession mMockingSession;
+ private WindowContainer.RemoteToken mRootTaskToken1;
+ private WindowContainer.RemoteToken mRootTaskToken2;
+
+ @Mock
+ private Task mWindowContainer1;
+ @Mock
+ private Task mWindowContainer2;
+
+ @Mock
+ private DisplayContent mDisplayContent;
+
+ @Mock
+ private Display mDisplay;
+
+ @Mock
+ private TaskDisplayArea mTda;
+
+ private final CarActivityInterceptorInterface mCarActivityInterceptorInterface =
+ new CarActivityInterceptorInterface() {
+ @Override
+ public int getUserAssignedToDisplay(int displayId) {
+ return DEFAULT_CURRENT_USER_ID;
+ }
+
+ @Override
+ public int getMainDisplayAssignedToUser(int userId) {
+ return 0;
+ }
+ };
+
+ @Before
+ public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+
+ mTda.mDisplayContent = mDisplayContent;
+ when(mDisplayContent.getDisplay()).thenReturn(mDisplay);
+ when(mDisplay.getDisplayId()).thenReturn(0);
+
+ mRootTaskToken1 = new WindowContainer.RemoteToken(mWindowContainer1);
+ mWindowContainer1.mRemoteToken = mRootTaskToken1;
+ when(mWindowContainer1.getTaskDisplayArea()).thenReturn(mTda);
+
+ mRootTaskToken2 = new WindowContainer.RemoteToken(mWindowContainer2);
+ when(mWindowContainer2.getTaskDisplayArea()).thenReturn(mTda);
+ mWindowContainer2.mRemoteToken = mRootTaskToken2;
+
+ mInterceptor = new CarActivityInterceptorUpdatableImpl(mCarActivityInterceptorInterface);
+ }
+
+ @After
+ public void tearDown() {
+ // If the exception is thrown during the MockingSession setUp, mMockingSession can be null.
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfo(String packageName,
+ String activityName, Intent intent, ActivityOptions options, int userId) {
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.packageName = packageName;
+ activityInfo.name = activityName;
+ ActivityInterceptorCallback.ActivityInterceptorInfo.Builder builder =
+ new ActivityInterceptorCallback.ActivityInterceptorInfo.Builder(
+ /* callingUId= */ 0, /* callingPid= */ 0, /* realCallingUid= */ 0,
+ /* realCallingPid= */ 0, /* userId= */ userId, intent,
+ new ResolveInfo(), activityInfo);
+ builder.setCheckedOptions(options);
+ return ActivityInterceptorInfoWrapper.create(builder.build());
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfoWithCustomIntent(
+ String packageName, String activityName, Intent intent) {
+ return createActivityInterceptorInfo(packageName, activityName, intent,
+ ActivityOptions.makeBasic(), DEFAULT_CURRENT_USER_ID);
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfoWithCustomIntent(
+ String packageName, String activityName, Intent intent, int userId) {
+ return createActivityInterceptorInfo(packageName, activityName, intent,
+ ActivityOptions.makeBasic(), userId);
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfoWithMainIntent(
+ String packageName, String activityName) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(ComponentName.unflattenFromString(packageName + "/" + activityName));
+ return createActivityInterceptorInfoWithCustomIntent(packageName, activityName, intent);
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfoWithMainIntent(
+ String packageName, String activityName, int userId) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(ComponentName.unflattenFromString(packageName + "/" + activityName));
+ return createActivityInterceptorInfoWithCustomIntent(packageName, activityName, intent,
+ userId);
+ }
+
+ private ActivityInterceptorInfoWrapper createActivityInterceptorInfoWithMainIntent(
+ String packageName, String activityName, ActivityOptions options) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(ComponentName.unflattenFromString(packageName + "/" + activityName));
+ return createActivityInterceptorInfo(packageName, activityName, intent, options,
+ DEFAULT_CURRENT_USER_ID);
+ }
+
+ @Test
+ public void interceptActivityLaunch_nullIntent_returnsNull() {
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithCustomIntent("com.example.app3",
+ "com.example.app3.MainActivity", /* intent= */ null);
+
+ ActivityInterceptResultWrapper result =
+ mInterceptor.onInterceptActivityLaunch(info);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void interceptActivityLaunch_unknownActivity_returnsNull() {
+ List<ComponentName> activities = List.of(
+ ComponentName.unflattenFromString("com.example.app/com.example.app.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app2/com.example.app2.MainActivity")
+ );
+ mInterceptor.setPersistentActivityOnRootTask(activities, mRootTaskToken1);
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithMainIntent("com.example.app3",
+ "com.example.app3.MainActivity");
+
+ ActivityInterceptResultWrapper result =
+ mInterceptor.onInterceptActivityLaunch(info);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void interceptActivityLaunch_nullOptions_persistedActivity_setsLaunchRootTask() {
+ List<ComponentName> activities = List.of(
+ ComponentName.unflattenFromString("com.example.app/com.example.app.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app2/com.example.app2.MainActivity")
+ );
+ mInterceptor.setPersistentActivityOnRootTask(activities, mRootTaskToken1);
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithMainIntent(activities.get(0).getPackageName(),
+ activities.get(0).getClassName(), /* options= */ null);
+
+ ActivityInterceptResultWrapper result =
+ mInterceptor.onInterceptActivityLaunch(info);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getInterceptResult().getActivityOptions().getLaunchRootTask())
+ .isEqualTo(WindowContainer.fromBinder(mRootTaskToken1)
+ .mRemoteToken.toWindowContainerToken());
+ }
+
+ @Test
+ public void interceptActivityLaunch_persistedActivity_setsLaunchRootTask() {
+ List<ComponentName> activities = List.of(
+ ComponentName.unflattenFromString("com.example.app/com.example.app.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app2/com.example.app2.MainActivity")
+ );
+ mInterceptor.setPersistentActivityOnRootTask(activities, mRootTaskToken1);
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithMainIntent(activities.get(0).getPackageName(),
+ activities.get(0).getClassName());
+
+ ActivityInterceptResultWrapper result =
+ mInterceptor.onInterceptActivityLaunch(info);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getInterceptResult().getActivityOptions().getLaunchRootTask())
+ .isEqualTo(WindowContainer.fromBinder(mRootTaskToken1)
+ .mRemoteToken.toWindowContainerToken());
+ }
+
+ @Test
+ public void interceptActivityLaunch_persistedActivity_differentUser_doesNothing() {
+ List<ComponentName> activities = List.of(
+ ComponentName.unflattenFromString("com.example.app/com.example.app.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app2/com.example.app2.MainActivity")
+ );
+ mInterceptor.setPersistentActivityOnRootTask(activities, mRootTaskToken1);
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithMainIntent(activities.get(0).getPackageName(),
+ activities.get(0).getClassName(), /* userId= */ PASSENGER_USER_ID);
+
+ ActivityInterceptResultWrapper result =
+ mInterceptor.onInterceptActivityLaunch(info);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void setPersistentActivity_nullLaunchRootTask_removesAssociation() {
+ List<ComponentName> activities1 = List.of(
+ ComponentName.unflattenFromString("com.example.app/com.example.app.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app2/com.example.app2.MainActivity")
+ );
+ List<ComponentName> activities2 = List.of(
+ ComponentName.unflattenFromString("com.example.app3/com.example.app3.MainActivity"),
+ ComponentName.unflattenFromString("com.example.app4/com.example.app4.MainActivity")
+ );
+ mInterceptor.setPersistentActivityOnRootTask(activities1, mRootTaskToken1);
+ mInterceptor.setPersistentActivityOnRootTask(activities2, mRootTaskToken2);
+ ActivityInterceptorInfoWrapper info =
+ createActivityInterceptorInfoWithMainIntent(activities1.get(0).getPackageName(),
+ activities1.get(0).getClassName());
+
+ mInterceptor.setPersistentActivityOnRootTask(activities1, null);
+
+ ActivityInterceptResultWrapper result = mInterceptor.onInterceptActivityLaunch(info);
+ assertThat(result).isNull();
+ assertThat(mInterceptor.getActivityToRootTaskMap()).containsExactly(
+ activities2.get(0), mRootTaskToken2,
+ activities2.get(1), mRootTaskToken2
+ );
+ }
+}
diff --git a/updatableServices/tests/src/com/android/server/wm/CarLaunchParamsModifierUpdatableTest.java b/updatableServices/tests/src/com/android/server/wm/CarLaunchParamsModifierUpdatableTest.java
index 13dc733..e59f9f9 100644
--- a/updatableServices/tests/src/com/android/server/wm/CarLaunchParamsModifierUpdatableTest.java
+++ b/updatableServices/tests/src/com/android/server/wm/CarLaunchParamsModifierUpdatableTest.java
@@ -50,8 +50,6 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.view.Display;
@@ -64,6 +62,7 @@ import com.android.internal.policy.AttributeCache;
import com.android.server.LocalServices;
import com.android.server.display.color.ColorDisplayService;
import com.android.server.input.InputManagerService;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import org.junit.After;
@@ -84,8 +83,11 @@ import java.util.function.Function;
*/
@RunWith(AndroidJUnit4.class)
public class CarLaunchParamsModifierUpdatableTest {
+ // TODO(b/262267582): Use these constants directly in the tests and remove the corresponding
+ // mock variables.
private static final int PASSENGER_DISPLAY_ID_10 = 10;
private static final int PASSENGER_DISPLAY_ID_11 = 11;
+ private static final int RANDOM_DISPLAY_ID_99 = 99;
private static final int VIRTUAL_DISPLAY_ID_2 = 2;
private static final int FEATURE_MAP_ID = 1111;
@@ -117,26 +119,23 @@ public class CarLaunchParamsModifierUpdatableTest {
private PackageConfigPersister mPackageConfigPersister;
@Mock
private InputManagerService mInputManagerService;
+ @Mock
+ private UserManagerInternal mUserManagerInternal;
@Mock
private Display mDisplay0ForDriver;
- @Mock
private TaskDisplayArea mDisplayArea0ForDriver;
@Mock
private Display mDisplay1Private;
- @Mock
private TaskDisplayArea mDisplayArea1Private;
@Mock
private Display mDisplay10ForPassenger;
- @Mock
private TaskDisplayArea mDisplayArea10ForPassenger;
@Mock
private Display mDisplay11ForPassenger;
- @Mock
private TaskDisplayArea mDisplayArea11ForPassenger;
@Mock
private Display mDisplay2Virtual;
- @Mock
private TaskDisplayArea mDisplayArea2Virtual;
private TaskDisplayArea mMapTaskDisplayArea;
@@ -157,8 +156,7 @@ public class CarLaunchParamsModifierUpdatableTest {
@Mock
private LaunchParamsController.LaunchParams mOutParams;
- private void mockDisplay(Display display, TaskDisplayArea defaultTaskDisplayArea,
- int displayId, int flags, int type) {
+ private TaskDisplayArea mockDisplay(Display display, int displayId, int flags, int type) {
when(mDisplayManager.getDisplay(displayId)).thenReturn(display);
when(display.getDisplayId()).thenReturn(displayId);
when(display.getFlags()).thenReturn(flags);
@@ -166,12 +164,14 @@ public class CarLaunchParamsModifierUpdatableTest {
// Return the same id as the display for simplicity
DisplayContent dc = mock(DisplayContent.class);
- defaultTaskDisplayArea.mDisplayContent = dc;
- when(defaultTaskDisplayArea.getDisplayContent()).thenReturn(dc);
+ TaskDisplayArea defaultTaskDisplayArea = new TaskDisplayArea(dc, mWindowManagerService,
+ "defaultTDA#" + displayId, DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER);
+ when(mRootWindowContainer.getDisplayContent(displayId)).thenReturn(dc);
when(mRootWindowContainer.getDisplayContentOrCreate(displayId)).thenReturn(dc);
when(dc.getDisplay()).thenReturn(display);
when(dc.getDefaultTaskDisplayArea()).thenReturn(defaultTaskDisplayArea);
when(dc.isTrusted()).thenReturn((flags & FLAG_TRUSTED) == FLAG_TRUSTED);
+ return defaultTaskDisplayArea;
}
@Before
@@ -181,6 +181,7 @@ public class CarLaunchParamsModifierUpdatableTest {
.mockStatic(ActivityTaskManager.class)
.strictness(Strictness.LENIENT)
.startMocking();
+
mContext = getInstrumentation().getTargetContext();
spyOn(mContext);
doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
@@ -194,15 +195,26 @@ public class CarLaunchParamsModifierUpdatableTest {
mActivityTaskManagerService.mPackageConfigPersister = mPackageConfigPersister;
mActivityTaskManagerService.mWindowOrganizerController =
new WindowOrganizerController(mActivityTaskManagerService);
+ mActivityTaskManagerService.mContext = mContext;
when(mActivityTaskManagerService.getTransitionController()).thenCallRealMethod();
when(mActivityTaskManagerService.getRecentTasks()).thenReturn(mRecentTasks);
when(mActivityTaskManagerService.getGlobalLock()).thenReturn(mWindowManagerGlobalLock);
+ when(mActivityTaskManagerService.getUiContext()).thenReturn(mContext);
+
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, mUserManagerInternal);
+ when(mUserManagerInternal.getUserAssignedToDisplay(anyInt()))
+ .thenReturn(UserHandle.USER_NULL);
+ when(mUserManagerInternal.getMainDisplayAssignedToUser(anyInt()))
+ .thenReturn(INVALID_DISPLAY);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ LocalServices.removeServiceForTest(ImeTargetVisibilityPolicy.class);
mWindowManagerService = WindowManagerService.main(
- mContext, mInputManagerService, /* showBootMsgs= */ false, /* onlyCore= */ false,
- /* policy= */ null, mActivityTaskManagerService,
+ mContext, mInputManagerService, /* showBootMsgs= */ false, /* policy= */ null,
+ mActivityTaskManagerService,
/* displayWindowSettingsProvider= */ null, () -> new SurfaceControl.Transaction(),
- /* surfaceFactory= */ null, /* surfaceControlFactory= */ null);
+ /* surfaceControlFactory= */ null);
mActivityTaskManagerService.mWindowManager = mWindowManagerService;
mRootWindowContainer.mWindowManager = mWindowManagerService;
@@ -210,7 +222,7 @@ public class CarLaunchParamsModifierUpdatableTest {
LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
mColorDisplayServiceInternal);
when(mActivityOptions.getLaunchDisplayId()).thenReturn(INVALID_DISPLAY);
- mockDisplay(mDisplay0ForDriver, mDisplayArea0ForDriver, DEFAULT_DISPLAY,
+ mDisplayArea0ForDriver = mockDisplay(mDisplay0ForDriver, DEFAULT_DISPLAY,
FLAG_TRUSTED, /* type= */ 0);
DisplayContent defaultDC = mRootWindowContainer.getDisplayContentOrCreate(DEFAULT_DISPLAY);
mMapTaskDisplayArea = new TaskDisplayArea(
@@ -221,13 +233,13 @@ public class CarLaunchParamsModifierUpdatableTest {
}).when(defaultDC).getItemFromTaskDisplayAreas(any());
when(mActivityRecordSource.getDisplayContent()).thenReturn(defaultDC);
- mockDisplay(mDisplay10ForPassenger, mDisplayArea10ForPassenger, PASSENGER_DISPLAY_ID_10,
+ mDisplayArea10ForPassenger = mockDisplay(mDisplay10ForPassenger, PASSENGER_DISPLAY_ID_10,
FLAG_TRUSTED, /* type= */ 0);
- mockDisplay(mDisplay11ForPassenger, mDisplayArea11ForPassenger, PASSENGER_DISPLAY_ID_11,
+ mDisplayArea11ForPassenger = mockDisplay(mDisplay11ForPassenger, PASSENGER_DISPLAY_ID_11,
FLAG_TRUSTED, /* type= */ 0);
- mockDisplay(mDisplay1Private, mDisplayArea1Private, 1,
+ mDisplayArea1Private = mockDisplay(mDisplay1Private, 1,
FLAG_TRUSTED | FLAG_PRIVATE, /* type= */ 0);
- mockDisplay(mDisplay2Virtual, mDisplayArea2Virtual, VIRTUAL_DISPLAY_ID_2,
+ mDisplayArea2Virtual = mockDisplay(mDisplay2Virtual, VIRTUAL_DISPLAY_ID_2,
FLAG_PRIVATE, /* type= */ 0);
mModifier = new CarLaunchParamsModifier(mContext);
@@ -242,7 +254,10 @@ public class CarLaunchParamsModifierUpdatableTest {
LocalServices.removeServiceForTest(WindowManagerInternal.class);
LocalServices.removeServiceForTest(WindowManagerPolicy.class);
LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
- mMockingSession.finishMocking();
+ // If the exception is thrown during the MockingSession setUp, mMockingSession can be null.
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
}
private void assertDisplayIsAllowed(@UserIdInt int userId, Display display) {
@@ -250,8 +265,8 @@ public class CarLaunchParamsModifierUpdatableTest {
mCurrentParams.mPreferredTaskDisplayArea = mBuiltin.getDefaultTaskDisplayAreaOnDisplay(
display.getDisplayId()).getTaskDisplayArea();
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, mActivityOptions, null /* request */, 0, mCurrentParams,
- mOutParams))
+ mActivityRecordSource, mActivityOptions, /* request= */ null , /* phase= */ 0,
+ mCurrentParams, mOutParams))
.isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_SKIP);
}
@@ -265,8 +280,8 @@ public class CarLaunchParamsModifierUpdatableTest {
displayAssigned.getDisplayId()).getTaskDisplayArea();
mCurrentParams.mPreferredTaskDisplayArea = requestedTaskDisplayArea;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, mActivityOptions, null /* request */, 0, mCurrentParams,
- mOutParams))
+ mActivityRecordSource, mActivityOptions, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams))
.isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea).isEqualTo(assignedTaskDisplayArea);
}
@@ -278,8 +293,8 @@ public class CarLaunchParamsModifierUpdatableTest {
}
mCurrentParams.mPreferredTaskDisplayArea = null;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, mActivityOptions, null /* request */, 0, mCurrentParams,
- mOutParams))
+ mActivityRecordSource, mActivityOptions, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams))
.isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea).isEqualTo(expectedDisplayArea);
}
@@ -288,8 +303,8 @@ public class CarLaunchParamsModifierUpdatableTest {
mTask.mUserId = userId;
mCurrentParams.mPreferredTaskDisplayArea = null;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, mActivityOptions, null /* request */, 0, mCurrentParams,
- mOutParams))
+ mActivityRecordSource, mActivityOptions, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams))
.isEqualTo(LaunchParamsController.LaunchParamsModifier.RESULT_SKIP);
assertThat(mOutParams.mPreferredTaskDisplayArea).isNull();
}
@@ -615,7 +630,7 @@ public class CarLaunchParamsModifierUpdatableTest {
@Test
public void testSourceDisplayFromProcessDisplayIfAvailable() {
- int userId = 10;
+ int userId = 108;
String processName = "processName";
int processUid = 11;
when(mActivityRecordActivity.getProcessName())
@@ -635,15 +650,15 @@ public class CarLaunchParamsModifierUpdatableTest {
mTask.mUserId = userId;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, null, null /* request */, 0, mCurrentParams, mOutParams))
- .isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
+ mActivityRecordSource, /* options= */ null, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams)).isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea)
.isEqualTo(mDisplayArea10ForPassenger);
}
@Test
public void testSourceDisplayFromLaunchingDisplayIfAvailable() {
- int userId = 10;
+ int userId = 108;
int launchedFromPid = 1324;
int launchedFromUid = 325;
when(mActivityRecordActivity.getLaunchedFromPid())
@@ -660,18 +675,18 @@ public class CarLaunchParamsModifierUpdatableTest {
when(controller.getTopActivityDisplayArea())
.thenReturn(mDisplayArea10ForPassenger);
mCurrentParams.mPreferredTaskDisplayArea = null;
- mTask.mUserId = 10;
+ mTask.mUserId = userId;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, null, null /* request */, 0, mCurrentParams, mOutParams))
- .isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
+ mActivityRecordSource, /* options= */ null, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams)).isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea)
.isEqualTo(mDisplayArea10ForPassenger);
}
@Test
public void testSourceDisplayFromCallingDisplayIfAvailable() {
- int userId = 10;
+ int userId = 108;
ActivityStarter.Request request = fakeRequest();
mUpdatable.setPassengerDisplays(new int[]{mDisplay11ForPassenger.getDisplayId(),
mDisplay10ForPassenger.getDisplayId()});
@@ -687,8 +702,8 @@ public class CarLaunchParamsModifierUpdatableTest {
mTask.mUserId = userId;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, null, request, 0, mCurrentParams, mOutParams))
- .isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
+ mActivityRecordSource, /* options= */ null, request, /* phase= */ 0, mCurrentParams,
+ mOutParams)).isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea)
.isEqualTo(mDisplayArea10ForPassenger);
}
@@ -709,7 +724,8 @@ public class CarLaunchParamsModifierUpdatableTest {
mTask.mUserId = 10;
assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
- mActivityRecordSource, null, request, 0, mCurrentParams, mOutParams))
+ mActivityRecordSource, /* options= */ null, request, /* phase= */ 0, mCurrentParams,
+ mOutParams))
.isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
assertThat(mOutParams.mPreferredTaskDisplayArea)
.isEqualTo(mDisplayArea11ForPassenger);
@@ -772,6 +788,62 @@ public class CarLaunchParamsModifierUpdatableTest {
DisplayAreaOrganizer.FEATURE_UNDEFINED));
}
+ @Test
+ public void testWindowingMode_forPassengerActivityOptions_updatedInParams() {
+ int userId = 108;
+ int launchedFromPid = 1324;
+ int launchedFromUid = 325;
+ when(mActivityOptions.getLaunchWindowingMode()).thenReturn(6);
+ when(mActivityRecordActivity.getLaunchedFromPid()).thenReturn(launchedFromPid);
+ when(mActivityRecordActivity.getLaunchedFromUid()).thenReturn(launchedFromUid);
+ mUpdatable.setPassengerDisplays(new int[]{PASSENGER_DISPLAY_ID_11,
+ PASSENGER_DISPLAY_ID_10});
+ mUpdatable.setDisplayAllowListForUser(userId, new int[]{PASSENGER_DISPLAY_ID_10});
+ WindowProcessController controller = mock(WindowProcessController.class);
+ when(mActivityTaskManagerService.getProcessController(launchedFromPid, launchedFromUid))
+ .thenReturn(controller);
+ when(controller.getTopActivityDisplayArea()).thenReturn(mDisplayArea10ForPassenger);
+ mCurrentParams.mPreferredTaskDisplayArea = null;
+ mTask.mUserId = 108;
+
+ assertThat(mModifier.onCalculate(mTask, mWindowLayout, mActivityRecordActivity,
+ mActivityRecordSource, mActivityOptions, /* request= */ null, /* phase= */ 0,
+ mCurrentParams, mOutParams)).isEqualTo(TaskLaunchParamsModifier.RESULT_DONE);
+ assertThat(mOutParams.mPreferredTaskDisplayArea).isEqualTo(mDisplayArea10ForPassenger);
+ assertThat(mOutParams.mWindowingMode).isEqualTo(6);
+ }
+
+ @Test
+ public void testVisibleUserStartsButNoOccupantZoneIsAssigned() {
+ // We have a Passenger display, but a visible user is started, but not an occupant zone is
+ // assigned yet. This happens for Home when a visible user is started.
+ mUpdatable.setPassengerDisplays(
+ new int[]{PASSENGER_DISPLAY_ID_10, PASSENGER_DISPLAY_ID_11});
+ int visibleUserId = 100;
+ when(mUserManagerInternal.getUserAssignedToDisplay(PASSENGER_DISPLAY_ID_11))
+ .thenReturn(visibleUserId);
+ when(mActivityOptions.getLaunchDisplayId()).thenReturn(PASSENGER_DISPLAY_ID_11);
+
+ // CarLaunchParamsModifier admires the launchDisplayId, not assigning a display.
+ assertNoDisplayIsAssigned(visibleUserId);
+ }
+
+ @Test
+ public void testVisibleUserUsesMainDisplayAsFallback_whenLaunchedOnRandomDisplay() {
+ mUpdatable.setPassengerDisplays(
+ new int[]{PASSENGER_DISPLAY_ID_10, PASSENGER_DISPLAY_ID_11});
+ int visibleUserId = 100;
+ when(mUserManagerInternal.getUserAssignedToDisplay(PASSENGER_DISPLAY_ID_11))
+ .thenReturn(visibleUserId);
+ when(mUserManagerInternal.getMainDisplayAssignedToUser(visibleUserId))
+ .thenReturn(PASSENGER_DISPLAY_ID_11);
+ // Try to start Activity on the non-main display.
+ when(mActivityOptions.getLaunchDisplayId()).thenReturn(RANDOM_DISPLAY_ID_99);
+
+ // For the visible user, fallbacks to the main display.
+ assertDisplayIsAssigned(visibleUserId, mDisplayArea11ForPassenger);
+ }
+
private static ActivityStarter.Request fakeRequest() {
ActivityStarter.Request request = new ActivityStarter.Request();
request.realCallingPid = 1324;