Merge "Add unit tests for InputMethodManagerService"
This commit is contained in:
committed by
Android (Google) Code Review
commit
c0cb906dcc
@@ -144,6 +144,7 @@ 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;
|
||||
@@ -1592,8 +1593,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
private final InputMethodManagerService mService;
|
||||
|
||||
public Lifecycle(Context context) {
|
||||
this(context, new InputMethodManagerService(context));
|
||||
}
|
||||
|
||||
public Lifecycle(
|
||||
Context context, @NonNull InputMethodManagerService inputMethodManagerService) {
|
||||
super(context);
|
||||
mService = new InputMethodManagerService(context);
|
||||
mService = inputMethodManagerService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1668,12 +1674,25 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
}
|
||||
|
||||
public InputMethodManagerService(Context context) {
|
||||
this(context, null, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
InputMethodManagerService(
|
||||
Context context,
|
||||
@Nullable ServiceThread serviceThreadForTesting,
|
||||
@Nullable InputMethodBindingController bindingControllerForTesting) {
|
||||
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 = new ServiceThread(
|
||||
HANDLER_THREAD_NAME, Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
|
||||
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);
|
||||
// Note: SettingsObserver doesn't register observers in its constructor.
|
||||
@@ -1701,10 +1720,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
|
||||
updateCurrentProfileIds();
|
||||
AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
|
||||
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
|
||||
mSettings, context);
|
||||
mSwitchingController =
|
||||
InputMethodSubtypeSwitchingController.createInstanceLocked(mSettings, context);
|
||||
mMenuController = new InputMethodMenuController(this);
|
||||
mBindingController = new InputMethodBindingController(this);
|
||||
mBindingController =
|
||||
bindingControllerForTesting != null
|
||||
? bindingControllerForTesting
|
||||
: new InputMethodBindingController(this);
|
||||
mAutofillController = new AutofillSuggestionsController(this);
|
||||
mPreventImeStartupUnlessTextEditor = mRes.getBoolean(
|
||||
com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
|
||||
@@ -3673,6 +3695,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
// UI for input.
|
||||
if (isTextEditor && editorInfo != null
|
||||
&& shouldRestoreImeVisibility(windowToken, softInputMode)) {
|
||||
if (DEBUG) Slog.v(TAG, "Will show input to restore visibility");
|
||||
res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection,
|
||||
editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion,
|
||||
imeDispatcher);
|
||||
@@ -3719,11 +3742,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
imeDispatcher);
|
||||
didStart = true;
|
||||
}
|
||||
showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
|
||||
showCurrentInputLocked(
|
||||
windowToken,
|
||||
InputMethodManager.SHOW_IMPLICIT,
|
||||
null,
|
||||
SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
|
||||
}
|
||||
break;
|
||||
case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
|
||||
if (DEBUG) {
|
||||
Slog.v(TAG, "Window asks to keep the input in whatever state it was last in");
|
||||
}
|
||||
// Do nothing.
|
||||
break;
|
||||
case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
|
||||
@@ -3794,6 +3823,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
// To maintain compatibility, we are now hiding the IME when we don't have
|
||||
// an editor upon refocusing a window.
|
||||
if (startInputByWinGainedFocus) {
|
||||
if (DEBUG) Slog.v(TAG, "Same window without editor will hide input");
|
||||
hideCurrentInputLocked(mCurFocusedWindow, 0, null,
|
||||
SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR);
|
||||
}
|
||||
@@ -3807,6 +3837,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
// 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");
|
||||
hideCurrentInputLocked(mCurFocusedWindow, 0, null,
|
||||
SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR);
|
||||
}
|
||||
|
||||
62
services/tests/InputMethodSystemServerTests/Android.bp
Normal file
62
services/tests/InputMethodSystemServerTests/Android.bp
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (C) 2022 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "FrameworksInputMethodSystemServerTests",
|
||||
defaults: [
|
||||
"modules-utils-testable-device-config-defaults",
|
||||
],
|
||||
|
||||
srcs: [
|
||||
"src/**/*.java",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"androidx.test.core",
|
||||
"androidx.test.runner",
|
||||
"androidx.test.espresso.core",
|
||||
"androidx.test.espresso.contrib",
|
||||
"androidx.test.ext.truth",
|
||||
"frameworks-base-testutils",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
"platform-test-annotations",
|
||||
"services.core",
|
||||
"servicestests-core-utils",
|
||||
"servicestests-utils-mockito-extended",
|
||||
"truth-prebuilt",
|
||||
],
|
||||
|
||||
libs: [
|
||||
"android.test.mock",
|
||||
"android.test.base",
|
||||
"android.test.runner",
|
||||
],
|
||||
|
||||
certificate: "platform",
|
||||
platform_apis: true,
|
||||
test_suites: ["device-tests"],
|
||||
|
||||
optimize: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.frameworks.inputmethodtests">
|
||||
|
||||
<uses-sdk android:targetSdkVersion="31" />
|
||||
|
||||
<!-- Permissions required for granting and logging -->
|
||||
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
|
||||
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
|
||||
<uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG"/>
|
||||
<uses-permission android:name="android.permission.OVERRIDE_COMPAT_CHANGE_CONFIG_ON_RELEASE_BUILD"/>
|
||||
|
||||
<!-- Permissions for reading system info -->
|
||||
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
|
||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<application android:testOnly="true"
|
||||
android:debuggable="true">
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.frameworks.inputmethodtests"
|
||||
android:label="Frameworks InputMethod System Service Tests" />
|
||||
|
||||
</manifest>
|
||||
39
services/tests/InputMethodSystemServerTests/AndroidTest.xml
Normal file
39
services/tests/InputMethodSystemServerTests/AndroidTest.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<configuration description="Runs Frameworks InputMethod System Services Tests.">
|
||||
<option name="test-suite-tag" value="apct" />
|
||||
<option name="test-suite-tag" value="apct-instrumentation" />
|
||||
|
||||
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
|
||||
<option name="cleanup-apks" value="true" />
|
||||
<option name="install-arg" value="-t" />
|
||||
<option name="test-file-name" value="FrameworksInputMethodSystemServerTests.apk" />
|
||||
</target_preparer>
|
||||
|
||||
<option name="test-tag" value="FrameworksInputMethodSystemServerTests" />
|
||||
|
||||
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
|
||||
<option name="package" value="com.android.frameworks.inputmethodtests" />
|
||||
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
|
||||
<option name="hidden-api-checks" value="false"/>
|
||||
</test>
|
||||
|
||||
<!-- Collect the files in the dump directory for debugging -->
|
||||
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
|
||||
<option name="directory-keys" value="/sdcard/FrameworksInputMethodSystemServerTests/" />
|
||||
<option name="collect-on-run-ended-only" value="true" />
|
||||
</metrics_collector>
|
||||
</configuration>
|
||||
1
services/tests/InputMethodSystemServerTests/OWNERS
Normal file
1
services/tests/InputMethodSystemServerTests/OWNERS
Normal file
@@ -0,0 +1 @@
|
||||
include /services/core/java/com/android/server/inputmethod/OWNERS
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* 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 com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.ActivityManagerInternal;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManagerInternal;
|
||||
import android.hardware.display.DisplayManagerInternal;
|
||||
import android.hardware.input.IInputManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.window.ImeOnBackInvokedDispatcher;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import com.android.internal.compat.IPlatformCompat;
|
||||
import com.android.internal.inputmethod.IInputMethod;
|
||||
import com.android.internal.inputmethod.IInputMethodClient;
|
||||
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.view.IInputMethodManager;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.ServiceThread;
|
||||
import com.android.server.SystemServerInitThreadPool;
|
||||
import com.android.server.SystemService;
|
||||
import com.android.server.input.InputManagerInternal;
|
||||
import com.android.server.pm.UserManagerInternal;
|
||||
import com.android.server.wm.WindowManagerInternal;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
/** Base class for testing {@link InputMethodManagerService}. */
|
||||
public class InputMethodManagerServiceTestBase {
|
||||
protected static final String TEST_SELECTED_IME_ID = "test.ime";
|
||||
protected static final String TEST_EDITOR_PKG_NAME = "test.editor";
|
||||
protected static final String TEST_FOCUSED_WINDOW_NAME = "test.editor/activity";
|
||||
protected static final WindowManagerInternal.ImeTargetInfo TEST_IME_TARGET_INFO =
|
||||
new WindowManagerInternal.ImeTargetInfo(
|
||||
TEST_FOCUSED_WINDOW_NAME,
|
||||
TEST_FOCUSED_WINDOW_NAME,
|
||||
TEST_FOCUSED_WINDOW_NAME,
|
||||
TEST_FOCUSED_WINDOW_NAME,
|
||||
TEST_FOCUSED_WINDOW_NAME);
|
||||
protected static final InputBindResult SUCCESS_WAITING_IME_BINDING_RESULT =
|
||||
new InputBindResult(
|
||||
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"0",
|
||||
0,
|
||||
null,
|
||||
false);
|
||||
|
||||
@Mock protected WindowManagerInternal mMockWindowManagerInternal;
|
||||
@Mock protected ActivityManagerInternal mMockActivityManagerInternal;
|
||||
@Mock protected PackageManagerInternal mMockPackageManagerInternal;
|
||||
@Mock protected InputManagerInternal mMockInputManagerInternal;
|
||||
@Mock protected DisplayManagerInternal mMockDisplayManagerInternal;
|
||||
@Mock protected UserManagerInternal mMockUserManagerInternal;
|
||||
@Mock protected InputMethodBindingController mMockInputMethodBindingController;
|
||||
@Mock protected IInputMethodClient mMockInputMethodClient;
|
||||
@Mock protected IBinder mWindowToken;
|
||||
@Mock protected IRemoteInputConnection mMockRemoteInputConnection;
|
||||
@Mock protected IRemoteAccessibilityInputConnection mMockRemoteAccessibilityInputConnection;
|
||||
@Mock protected ImeOnBackInvokedDispatcher mMockImeOnBackInvokedDispatcher;
|
||||
@Mock protected IInputMethodManager.Stub mMockIInputMethodManager;
|
||||
@Mock protected IPlatformCompat.Stub mMockIPlatformCompat;
|
||||
@Mock protected IInputMethod mMockInputMethod;
|
||||
@Mock protected IBinder mMockInputMethodBinder;
|
||||
@Mock protected IInputManager mMockIInputManager;
|
||||
|
||||
protected Context mContext;
|
||||
protected MockitoSession mMockingSession;
|
||||
protected int mTargetSdkVersion;
|
||||
protected int mCallingUserId;
|
||||
protected EditorInfo mEditorInfo;
|
||||
protected IInputMethodInvoker mMockInputMethodInvoker;
|
||||
protected InputMethodManagerService mInputMethodManagerService;
|
||||
protected ServiceThread mServiceThread;
|
||||
|
||||
@Before
|
||||
public void setUp() throws RemoteException {
|
||||
mMockingSession =
|
||||
mockitoSession()
|
||||
.initMocks(this)
|
||||
.strictness(Strictness.LENIENT)
|
||||
.mockStatic(LocalServices.class)
|
||||
.mockStatic(ServiceManager.class)
|
||||
.mockStatic(SystemServerInitThreadPool.class)
|
||||
.startMocking();
|
||||
|
||||
mContext = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
spyOn(mContext);
|
||||
|
||||
mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
|
||||
mCallingUserId = UserHandle.getCallingUserId();
|
||||
mEditorInfo = new EditorInfo();
|
||||
mEditorInfo.packageName = TEST_EDITOR_PKG_NAME;
|
||||
|
||||
// Injecting and mocking local services.
|
||||
doReturn(mMockWindowManagerInternal)
|
||||
.when(() -> LocalServices.getService(WindowManagerInternal.class));
|
||||
doReturn(mMockActivityManagerInternal)
|
||||
.when(() -> LocalServices.getService(ActivityManagerInternal.class));
|
||||
doReturn(mMockPackageManagerInternal)
|
||||
.when(() -> LocalServices.getService(PackageManagerInternal.class));
|
||||
doReturn(mMockInputManagerInternal)
|
||||
.when(() -> LocalServices.getService(InputManagerInternal.class));
|
||||
doReturn(mMockDisplayManagerInternal)
|
||||
.when(() -> LocalServices.getService(DisplayManagerInternal.class));
|
||||
doReturn(mMockUserManagerInternal)
|
||||
.when(() -> LocalServices.getService(UserManagerInternal.class));
|
||||
doReturn(mMockIInputMethodManager)
|
||||
.when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
|
||||
doReturn(mMockIPlatformCompat)
|
||||
.when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
|
||||
|
||||
// Stubbing out context related methods to avoid the system holding strong references to
|
||||
// InputMethodManagerService.
|
||||
doNothing().when(mContext).enforceCallingPermission(anyString(), anyString());
|
||||
doNothing().when(mContext).sendBroadcastAsUser(any(), any());
|
||||
doReturn(null).when(mContext).registerReceiver(any(), any());
|
||||
doReturn(null)
|
||||
.when(mContext)
|
||||
.registerReceiverAsUser(any(), any(), any(), anyString(), any(), anyInt());
|
||||
|
||||
// Injecting and mocked InputMethodBindingController and InputMethod.
|
||||
mMockInputMethodInvoker = IInputMethodInvoker.create(mMockInputMethod);
|
||||
InputManager.resetInstance(mMockIInputManager);
|
||||
synchronized (ImfLock.class) {
|
||||
when(mMockInputMethodBindingController.getCurMethod())
|
||||
.thenReturn(mMockInputMethodInvoker);
|
||||
when(mMockInputMethodBindingController.bindCurrentMethod())
|
||||
.thenReturn(SUCCESS_WAITING_IME_BINDING_RESULT);
|
||||
doNothing().when(mMockInputMethodBindingController).unbindCurrentMethod();
|
||||
when(mMockInputMethodBindingController.getSelectedMethodId())
|
||||
.thenReturn(TEST_SELECTED_IME_ID);
|
||||
}
|
||||
|
||||
// Shuffling around all other initialization to make the test runnable.
|
||||
when(mMockIInputManager.getInputDeviceIds()).thenReturn(new int[0]);
|
||||
when(mMockIInputMethodManager.isImeTraceEnabled()).thenReturn(false);
|
||||
when(mMockIPlatformCompat.isChangeEnabledByUid(anyLong(), anyInt())).thenReturn(true);
|
||||
when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(true);
|
||||
when(mMockUserManagerInternal.getProfileIds(anyInt(), anyBoolean()))
|
||||
.thenReturn(new int[] {0});
|
||||
when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true);
|
||||
when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt()))
|
||||
.thenReturn(Binder.getCallingUid());
|
||||
when(mMockWindowManagerInternal.onToggleImeRequested(anyBoolean(), any(), any(), anyInt()))
|
||||
.thenReturn(TEST_IME_TARGET_INFO);
|
||||
when(mMockInputMethodClient.asBinder()).thenReturn(mMockInputMethodBinder);
|
||||
|
||||
// Used by lazy initializing draw IMS nav bar at InputMethodManagerService#systemRunning(),
|
||||
// which is ok to be mocked out for now.
|
||||
doReturn(null).when(() -> SystemServerInitThreadPool.submit(any(), anyString()));
|
||||
|
||||
mServiceThread =
|
||||
new ServiceThread(
|
||||
"TestServiceThread",
|
||||
Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
|
||||
false);
|
||||
mInputMethodManagerService =
|
||||
new InputMethodManagerService(
|
||||
mContext, mServiceThread, mMockInputMethodBindingController);
|
||||
|
||||
// Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
|
||||
// InputMethodManagerService, which is closer to the real situation.
|
||||
InputMethodManagerService.Lifecycle lifecycle =
|
||||
new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService);
|
||||
|
||||
// Public local InputMethodManagerService.
|
||||
lifecycle.onStart();
|
||||
try {
|
||||
// After this boot phase, services can broadcast Intents.
|
||||
lifecycle.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
|
||||
} catch (SecurityException e) {
|
||||
// Security exception to permission denial is expected in test, mocking out to ensure
|
||||
// InputMethodManagerService as system ready state.
|
||||
if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Call InputMethodManagerService#addClient() as a preparation to start interacting with it.
|
||||
mInputMethodManagerService.addClient(mMockInputMethodClient, mMockRemoteInputConnection, 0);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (mServiceThread != null) {
|
||||
mServiceThread.quitSafely();
|
||||
}
|
||||
|
||||
if (mMockingSession != null) {
|
||||
mMockingSession.finishMocking();
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)
|
||||
throws RemoteException {
|
||||
synchronized (ImfLock.class) {
|
||||
verify(mMockInputMethodBindingController, times(setVisible ? 1 : 0))
|
||||
.setCurrentMethodVisible();
|
||||
}
|
||||
verify(mMockInputMethod, times(showSoftInput ? 1 : 0))
|
||||
.showSoftInput(any(), anyInt(), any());
|
||||
}
|
||||
|
||||
protected void verifyHideSoftInput(boolean setNotVisible, boolean hideSoftInput)
|
||||
throws RemoteException {
|
||||
synchronized (ImfLock.class) {
|
||||
verify(mMockInputMethodBindingController, times(setNotVisible ? 1 : 0))
|
||||
.setCurrentMethodNotVisible();
|
||||
}
|
||||
verify(mMockInputMethod, times(hideSoftInput ? 1 : 0))
|
||||
.hideSoftInput(any(), anyInt(), any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
|
||||
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.window.ImeOnBackInvokedDispatcher;
|
||||
|
||||
import com.android.internal.inputmethod.IInputMethodClient;
|
||||
import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection;
|
||||
import com.android.internal.inputmethod.IRemoteInputConnection;
|
||||
import com.android.internal.inputmethod.InputBindResult;
|
||||
import com.android.internal.inputmethod.InputMethodDebug;
|
||||
import com.android.internal.inputmethod.StartInputFlags;
|
||||
import com.android.internal.inputmethod.StartInputReason;
|
||||
import com.android.server.wm.WindowManagerInternal;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Test the behavior of {@link InputMethodManagerService#startInputOrWindowGainedFocus(int,
|
||||
* IInputMethodClient, IBinder, int, int, int, EditorInfo, IRemoteInputConnection,
|
||||
* IRemoteAccessibilityInputConnection, int, int, ImeOnBackInvokedDispatcher)}.
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class InputMethodManagerServiceWindowGainedFocusTest
|
||||
extends InputMethodManagerServiceTestBase {
|
||||
private static final String TAG = "IMMSWindowGainedFocusTest";
|
||||
|
||||
private static final int[] SOFT_INPUT_STATE_FLAGS =
|
||||
new int[] {
|
||||
SOFT_INPUT_STATE_UNSPECIFIED,
|
||||
SOFT_INPUT_STATE_UNCHANGED,
|
||||
SOFT_INPUT_STATE_HIDDEN,
|
||||
SOFT_INPUT_STATE_ALWAYS_HIDDEN,
|
||||
SOFT_INPUT_STATE_VISIBLE,
|
||||
SOFT_INPUT_STATE_ALWAYS_VISIBLE
|
||||
};
|
||||
private static final int[] SOFT_INPUT_ADJUST_FLAGS =
|
||||
new int[] {
|
||||
SOFT_INPUT_ADJUST_UNSPECIFIED,
|
||||
SOFT_INPUT_ADJUST_RESIZE,
|
||||
SOFT_INPUT_ADJUST_PAN,
|
||||
SOFT_INPUT_ADJUST_NOTHING
|
||||
};
|
||||
private static final int DEFAULT_SOFT_INPUT_FLAG =
|
||||
StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
|
||||
|
||||
@Parameterized.Parameters(name = "softInputState={0}, softInputAdjustment={1}")
|
||||
public static List<Object[]> softInputModeConfigs() {
|
||||
ArrayList<Object[]> params = new ArrayList<>();
|
||||
for (int softInputState : SOFT_INPUT_STATE_FLAGS) {
|
||||
for (int softInputAdjust : SOFT_INPUT_ADJUST_FLAGS) {
|
||||
params.add(new Object[] {softInputState, softInputAdjust});
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private final int mSoftInputState;
|
||||
private final int mSoftInputAdjustment;
|
||||
|
||||
public InputMethodManagerServiceWindowGainedFocusTest(
|
||||
int softInputState, int softInputAdjustment) {
|
||||
mSoftInputState = softInputState;
|
||||
mSoftInputAdjustment = softInputAdjustment;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startInputOrWindowGainedFocus_forwardNavigation() throws RemoteException {
|
||||
mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
|
||||
|
||||
assertThat(
|
||||
startInputOrWindowGainedFocus(
|
||||
DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
|
||||
.isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
|
||||
|
||||
switch (mSoftInputState) {
|
||||
case SOFT_INPUT_STATE_UNSPECIFIED:
|
||||
boolean showSoftInput = mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE;
|
||||
verifyShowSoftInput(
|
||||
showSoftInput /* setVisible */, showSoftInput /* showSoftInput */);
|
||||
// Soft input was hidden by default, so it doesn't need to call
|
||||
// {@code IMS#hideSoftInput()}.
|
||||
verifyHideSoftInput(!showSoftInput /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_VISIBLE:
|
||||
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
|
||||
verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_HIDDEN:
|
||||
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
// Soft input was hidden by default, so it doesn't need to call
|
||||
// {@code IMS#hideSoftInput()}.
|
||||
verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unhandled soft input mode: "
|
||||
+ InputMethodDebug.softInputModeToString(mSoftInputState));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startInputOrWindowGainedFocus_notForwardNavigation() throws RemoteException {
|
||||
mockHasImeFocusAndRestoreImeVisibility(false /* restoreImeVisibility */);
|
||||
|
||||
assertThat(
|
||||
startInputOrWindowGainedFocus(
|
||||
DEFAULT_SOFT_INPUT_FLAG, false /* forwardNavigation */))
|
||||
.isEqualTo(SUCCESS_WAITING_IME_BINDING_RESULT);
|
||||
|
||||
switch (mSoftInputState) {
|
||||
case SOFT_INPUT_STATE_UNSPECIFIED:
|
||||
boolean hideSoftInput = mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE;
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
// Soft input was hidden by default, so it doesn't need to call
|
||||
// {@code IMS#hideSoftInput()}.
|
||||
verifyHideSoftInput(hideSoftInput /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_VISIBLE:
|
||||
case SOFT_INPUT_STATE_HIDDEN:
|
||||
case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
|
||||
verifyShowSoftInput(true /* setVisible */, true /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
// Soft input was hidden by default, so it doesn't need to call
|
||||
// {@code IMS#hideSoftInput()}.
|
||||
verifyHideSoftInput(true /* setNotVisible */, false /* hideSoftInput */);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unhandled soft input mode: "
|
||||
+ InputMethodDebug.softInputModeToString(mSoftInputState));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
|
||||
when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
|
||||
|
||||
assertThat(
|
||||
startInputOrWindowGainedFocus(
|
||||
DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
|
||||
.isEqualTo(InputBindResult.INVALID_USER);
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startInputOrWindowGainedFocus_invalidFocusStatus() throws RemoteException {
|
||||
int[] invalidImeClientFocus =
|
||||
new int[] {
|
||||
WindowManagerInternal.ImeClientFocusResult.NOT_IME_TARGET_WINDOW,
|
||||
WindowManagerInternal.ImeClientFocusResult.DISPLAY_ID_MISMATCH,
|
||||
WindowManagerInternal.ImeClientFocusResult.INVALID_DISPLAY_ID
|
||||
};
|
||||
InputBindResult[] inputBingResult =
|
||||
new InputBindResult[] {
|
||||
InputBindResult.NOT_IME_TARGET_WINDOW,
|
||||
InputBindResult.DISPLAY_ID_MISMATCH,
|
||||
InputBindResult.INVALID_DISPLAY_ID
|
||||
};
|
||||
|
||||
for (int i = 0; i < invalidImeClientFocus.length; i++) {
|
||||
when(mMockWindowManagerInternal.hasInputMethodClientFocus(
|
||||
any(), anyInt(), anyInt(), anyInt()))
|
||||
.thenReturn(invalidImeClientFocus[i]);
|
||||
|
||||
assertThat(
|
||||
startInputOrWindowGainedFocus(
|
||||
DEFAULT_SOFT_INPUT_FLAG, true /* forwardNavigation */))
|
||||
.isEqualTo(inputBingResult[i]);
|
||||
verifyShowSoftInput(false /* setVisible */, false /* showSoftInput */);
|
||||
verifyHideSoftInput(false /* setNotVisible */, false /* hideSoftInput */);
|
||||
}
|
||||
}
|
||||
|
||||
private InputBindResult startInputOrWindowGainedFocus(
|
||||
int startInputFlag, boolean forwardNavigation) {
|
||||
int softInputMode = mSoftInputState | mSoftInputAdjustment;
|
||||
if (forwardNavigation) {
|
||||
softInputMode |= SOFT_INPUT_IS_FORWARD_NAVIGATION;
|
||||
}
|
||||
|
||||
Log.i(
|
||||
TAG,
|
||||
"startInputOrWindowGainedFocus() softInputStateFlag="
|
||||
+ InputMethodDebug.softInputModeToString(mSoftInputState)
|
||||
+ ", softInputAdjustFlag="
|
||||
+ InputMethodDebug.softInputModeToString(mSoftInputAdjustment));
|
||||
|
||||
return mInputMethodManagerService.startInputOrWindowGainedFocus(
|
||||
StartInputReason.WINDOW_FOCUS_GAIN /* startInputReason */,
|
||||
mMockInputMethodClient /* client */,
|
||||
mWindowToken /* windowToken */,
|
||||
startInputFlag /* startInputFlags */,
|
||||
softInputMode /* softInputMode */,
|
||||
0 /* windowFlags */,
|
||||
mEditorInfo /* editorInfo */,
|
||||
mMockRemoteInputConnection /* inputConnection */,
|
||||
mMockRemoteAccessibilityInputConnection /* remoteAccessibilityInputConnection */,
|
||||
mTargetSdkVersion /* unverifiedTargetSdkVersion */,
|
||||
mCallingUserId /* userId */,
|
||||
mMockImeOnBackInvokedDispatcher /* imeDispatcher */);
|
||||
}
|
||||
|
||||
private void mockHasImeFocusAndRestoreImeVisibility(boolean restoreImeVisibility) {
|
||||
when(mMockWindowManagerInternal.hasInputMethodClientFocus(
|
||||
any(), anyInt(), anyInt(), anyInt()))
|
||||
.thenReturn(WindowManagerInternal.ImeClientFocusResult.HAS_IME_FOCUS);
|
||||
when(mMockWindowManagerInternal.shouldRestoreImeVisibility(any()))
|
||||
.thenReturn(restoreImeVisibility);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user