diff options
61 files changed, 12448 insertions, 260 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..f2d0a50 --- /dev/null +++ b/builtInServices/prebuilts/mu_imms-prebuilt.jar 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; |