Switch IME subtypes with keyboard shortcuts

Bug: 258853866
Test: atest ModifierShortcutTests SwitchKeyboardLayoutTest
Change-Id: I9c3c6ca326c1ee29e22d773136fcb9797eb0a047
This commit is contained in:
Adrian Roos
2023-01-09 11:10:07 +01:00
parent fc6304dc74
commit 00a86c83bf
6 changed files with 113 additions and 20 deletions

View File

@@ -177,6 +177,14 @@ public abstract class InputMethodManagerInternal {
*/
public abstract void unbindAccessibilityFromCurrentClient(int accessibilityConnectionId);
/**
* Switch the keyboard layout in response to a keyboard shortcut.
*
* @param direction {@code 1} to switch to the next subtype, {@code -1} to switch to the
* previous subtype.
*/
public abstract void switchKeyboardLayout(int direction);
/**
* Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing.
*/
@@ -256,6 +264,10 @@ public abstract class InputMethodManagerInternal {
@Override
public void maybeFinishStylusHandwriting() {
}
@Override
public void switchKeyboardLayout(int direction) {
}
};
/**

View File

@@ -3964,17 +3964,22 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
if (!calledWithValidTokenLocked(token)) {
return false;
}
final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype);
if (nextSubtype == null) {
return false;
}
setInputMethodWithSubtypeIdLocked(token, nextSubtype.mImi.getId(),
nextSubtype.mSubtypeId);
return true;
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) {
@@ -5622,6 +5627,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
mHandler.removeMessages(MSG_FINISH_HANDWRITING);
mHandler.obtainMessage(MSG_FINISH_HANDWRITING).sendToTarget();
}
@Override
public void switchKeyboardLayout(int direction) {
synchronized (ImfLock.class) {
if (direction > 0) {
switchToNextInputMethodLocked(null /* token */, true /* onlyCurrentIme */);
} else {
// TODO(b/258853866): Support backwards switching.
}
}
}
}
@BinderThread

View File

@@ -89,6 +89,7 @@ import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.RecentTaskInfo;
@@ -161,6 +162,7 @@ import android.service.dreams.IDreamManager;
import android.service.vr.IPersistentVrStateCallbacks;
import android.speech.RecognizerIntent;
import android.telecom.TelecomManager;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.MutableBoolean;
import android.util.PrintWriterPrinter;
@@ -396,7 +398,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
ActivityTaskManagerInternal mActivityTaskManagerInternal;
AutofillManagerInternal mAutofillManagerInternal;
InputManagerInternal mInputManagerInternal;
InputMethodManagerInternal mInputMethodManagerInternal;
DreamManagerInternal mDreamManagerInternal;
PowerManagerInternal mPowerManagerInternal;
IStatusBarService mStatusBarService;
@@ -656,6 +657,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private static final int MSG_HANDLE_ALL_APPS = 22;
private static final int MSG_LAUNCH_ASSIST = 23;
private static final int MSG_RINGER_TOGGLE_CHORD = 24;
private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 25;
private class PolicyHandler extends Handler {
@Override
@@ -726,6 +728,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case MSG_SCREENSHOT_CHORD:
handleScreenShot(msg.arg1);
break;
case MSG_SWITCH_KEYBOARD_LAYOUT:
handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
break;
}
}
}
@@ -1022,14 +1027,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
break;
case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: {
if (mDismissImeOnBackKeyPressed) {
if (mInputMethodManagerInternal == null) {
mInputMethodManagerInternal =
LocalServices.getService(InputMethodManagerInternal.class);
}
if (mInputMethodManagerInternal != null) {
mInputMethodManagerInternal.hideCurrentInputMethod(
InputMethodManagerInternal.get().hideCurrentInputMethod(
SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME);
}
} else {
shortPressPowerGoHome();
}
@@ -3162,7 +3161,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
if (down && repeatCount == 0) {
int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);
sendSwitchKeyboardLayout(event, direction);
return key_consumed;
}
break;
@@ -3412,7 +3411,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
KeyEvent.META_CTRL_ON)) {
int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);
sendSwitchKeyboardLayout(event, direction);
return true;
}
}
@@ -3438,6 +3437,19 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return false;
}
private void sendSwitchKeyboardLayout(@NonNull KeyEvent event, int direction) {
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, event.getDeviceId(),
direction).sendToTarget();
}
private void handleSwitchKeyboardLayout(int deviceId, int direction) {
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) {
InputMethodManagerInternal.get().switchKeyboardLayout(direction);
} else {
mWindowManagerFuncs.switchKeyboardLayout(deviceId, direction);
}
}
private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
int policyFlags) {
int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);

View File

@@ -127,7 +127,7 @@ public class InputMethodManagerServiceTestBase {
mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.mockStatic(LocalServices.class)
.spyStatic(LocalServices.class)
.mockStatic(ServiceManager.class)
.mockStatic(SystemServerInitThreadPool.class)
.startMocking();
@@ -212,6 +212,7 @@ public class InputMethodManagerServiceTestBase {
new InputMethodManagerService.Lifecycle(mContext, mInputMethodManagerService);
// Public local InputMethodManagerService.
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
lifecycle.onStart();
try {
// After this boot phase, services can broadcast Intents.
@@ -237,6 +238,7 @@ public class InputMethodManagerServiceTestBase {
if (mMockingSession != null) {
mMockingSession.finishMocking();
}
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
}
protected void verifyShowSoftInput(boolean setVisible, boolean showSoftInput)

View File

@@ -0,0 +1,39 @@
/*
* 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 org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class SwitchKeyboardLayoutTest extends InputMethodManagerServiceTestBase {
@Test
public void testSwitchToNextKeyboardLayout() {
ExtendedMockito.spyOn(mInputMethodManagerService.mSwitchingController);
InputMethodManagerInternal.get().switchKeyboardLayout(1);
verify(mInputMethodManagerService.mSwitchingController)
.getNextInputMethodLocked(eq(true) /* onlyCurrentIme */, any(), any());
}
}

View File

@@ -67,6 +67,7 @@ import android.os.RemoteException;
import android.os.Vibrator;
import android.service.dreams.DreamManagerInternal;
import android.telecom.TelecomManager;
import android.util.FeatureFlagUtils;
import android.view.Display;
import android.view.KeyEvent;
import android.view.autofill.AutofillManagerInternal;
@@ -76,6 +77,7 @@ import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.server.GestureLauncherService;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.vr.VrManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -114,6 +116,7 @@ class TestPhoneWindowManager {
@Mock private Vibrator mVibrator;
@Mock private PowerManager mPowerManager;
@Mock private WindowManagerPolicy.WindowManagerFuncs mWindowManagerFuncsImpl;
@Mock private InputMethodManagerInternal mInputMethodManagerInternal;
@Mock private AudioManagerInternal mAudioManagerInternal;
@Mock private SearchManager mSearchManager;
@@ -184,6 +187,8 @@ class TestPhoneWindowManager {
() -> LocalServices.getService(eq(GestureLauncherService.class)));
doReturn(null).when(() -> LocalServices.getService(eq(VrManagerInternal.class)));
doReturn(null).when(() -> LocalServices.getService(eq(AutofillManagerInternal.class)));
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
LocalServices.addService(InputMethodManagerInternal.class, mInputMethodManagerInternal);
doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class));
doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
@@ -242,6 +247,7 @@ class TestPhoneWindowManager {
void tearDown() {
mHandlerThread.quitSafely();
LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
mMockitoSession.finishMocking();
}
@@ -417,7 +423,13 @@ class TestPhoneWindowManager {
void assertSwitchKeyboardLayout(int direction) {
waitForIdle();
verify(mWindowManagerFuncsImpl).switchKeyboardLayout(anyInt(), eq(direction));
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) {
verify(mInputMethodManagerInternal).switchKeyboardLayout(eq(direction));
verify(mWindowManagerFuncsImpl, never()).switchKeyboardLayout(anyInt(), anyInt());
} else {
verify(mWindowManagerFuncsImpl).switchKeyboardLayout(anyInt(), eq(direction));
verify(mInputMethodManagerInternal, never()).switchKeyboardLayout(anyInt());
}
}
void assertTakeBugreport() {