Merge "Add unit tests for InputMethodManagerService"

This commit is contained in:
TreeHugger Robot
2022-11-01 00:39:51 +00:00
committed by Android (Google) Code Review
7 changed files with 707 additions and 7 deletions

View File

@@ -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);
}

View 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,
},
}

View File

@@ -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>

View 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>

View File

@@ -0,0 +1 @@
include /services/core/java/com/android/server/inputmethod/OWNERS

View File

@@ -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());
}
}

View File

@@ -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);
}
}