diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 0ea64abbe5487..8c7658e53dcd6 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -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) { + } }; /** diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 1226007abfb00..eba261ad6508b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4050,17 +4050,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) { @@ -5749,6 +5754,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 diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f3089315af722..37877cf8264e1 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -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; @@ -160,6 +161,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.MathUtils; import android.util.MutableBoolean; @@ -398,7 +400,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { ActivityTaskManagerInternal mActivityTaskManagerInternal; AutofillManagerInternal mAutofillManagerInternal; InputManagerInternal mInputManagerInternal; - InputMethodManagerInternal mInputMethodManagerInternal; DreamManagerInternal mDreamManagerInternal; PowerManagerInternal mPowerManagerInternal; IStatusBarService mStatusBarService; @@ -659,6 +660,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 @@ -729,6 +731,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; } } } @@ -1025,14 +1030,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(); } @@ -3170,7 +3169,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; @@ -3420,7 +3419,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; } } @@ -3446,6 +3445,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); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index dbdffd05714aa..9501b9604fa0f 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -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) diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/SwitchKeyboardLayoutTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/SwitchKeyboardLayoutTest.java new file mode 100644 index 0000000000000..111cabd298f58 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/SwitchKeyboardLayoutTest.java @@ -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()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 5cdaf0d7ed83f..b6939747a7b69 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -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() {