From 2210bdfb85d9d21ef814bdfa9be5eedc86e44219 Mon Sep 17 00:00:00 2001 From: Shu Chen <shuchen@google.com> Date: Thu, 16 Mar 2023 09:21:33 +0800 Subject: [PATCH] Supports showing Ime shortcuts in the system shortcuts UI. Bug: 273483670 Test: locally tested. Change-Id: I49a59ab8688aaf193a79c610738a1db951f2bfdf --- core/java/android/view/IWindowManager.aidl | 13 +++++ core/java/android/view/WindowManager.java | 15 ++++- core/java/android/view/WindowManagerImpl.java | 26 ++++++++- data/etc/services.core.protolog.json | 6 ++ .../statusbar/KeyboardShortcutListSearch.java | 57 +++++++++++++------ .../systemui/statusbar/KeyboardShortcuts.java | 52 ++++++++++++----- .../KeyboardShortcutListSearchTest.java | 1 + .../statusbar/KeyboardShortcutsTest.java | 1 + .../server/wm/WindowManagerService.java | 42 ++++++++++++-- .../server/wm/WindowManagerServiceTests.java | 52 +++++++++++++++++ 10 files changed, 226 insertions(+), 39 deletions(-) diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 209729b2a38b..6d96bb9423c5 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -471,9 +471,22 @@ interface IWindowManager * Requests Keyboard Shortcuts from the displayed window. * * @param receiver The receiver to deliver the results to. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * @see #requestImeKeyboardShortcuts(IResultReceiver, int) */ void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId); + /** + * Requests Keyboard Shortcuts from currently selected IME. + * + * @param receiver The receiver to deliver the results to. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * @see #requestAppKeyboardShortcuts(IResultReceiver, int) + */ + void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId); + /** * Retrieves the current stable insets from the primary display. */ diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f9f86f80666a..d702367965a1 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1384,14 +1384,27 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"; /** - * Request for keyboard shortcuts to be retrieved asynchronously. + * Request for app's keyboard shortcuts to be retrieved asynchronously. * * @param receiver The callback to be triggered when the result is ready. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. * * @hide */ public void requestAppKeyboardShortcuts(final KeyboardShortcutsReceiver receiver, int deviceId); + /** + * Request for ime's keyboard shortcuts to be retrieved asynchronously. + * + * @param receiver The callback to be triggered when the result is ready. + * @param deviceId The deviceId of KeyEvent by which this request is triggered, or -1 if it's + * not triggered by a KeyEvent. + * + * @hide + */ + default void requestImeKeyboardShortcuts(KeyboardShortcutsReceiver receiver, int deviceId) {}; + /** * Return the touch region for the current IME window, or an empty region if there is none. * diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index df3e0bb74292..b57163c4e435 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -215,14 +215,36 @@ public final class WindowManagerImpl implements WindowManager { @Override public void send(int resultCode, Bundle resultData) throws RemoteException { List<KeyboardShortcutGroup> result = - resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, android.view.KeyboardShortcutGroup.class); + resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, + android.view.KeyboardShortcutGroup.class); receiver.onKeyboardShortcutsReceived(result); } }; try { WindowManagerGlobal.getWindowManagerService() - .requestAppKeyboardShortcuts(resultReceiver, deviceId); + .requestAppKeyboardShortcuts(resultReceiver, deviceId); } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public void requestImeKeyboardShortcuts( + final KeyboardShortcutsReceiver receiver, int deviceId) { + IResultReceiver resultReceiver = new IResultReceiver.Stub() { + @Override + public void send(int resultCode, Bundle resultData) throws RemoteException { + List<KeyboardShortcutGroup> result = + resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY, + android.view.KeyboardShortcutGroup.class); + receiver.onKeyboardShortcutsReceived(result); + } + }; + try { + WindowManagerGlobal.getWindowManagerService() + .requestImeKeyboardShortcuts(resultReceiver, deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index e4defcfa359f..136d1a25255b 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -3379,6 +3379,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "975028389": { + "message": "unable to call receiver for empty keyboard shortcuts", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "975275467": { "message": "Set animatingExit: reason=remove\/isAnimating win=%s", "level": "VERBOSE", diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 06f43f1eeaa5..39181449aaa0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -56,7 +56,6 @@ import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.WindowManager.KeyboardShortcutsReceiver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; import android.widget.EditText; @@ -337,6 +336,12 @@ public final class KeyboardShortcutListSearch { mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変æ›"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変æ›"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "ã‹ãª"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift"); mModifierNames.put(KeyEvent.META_META_ON, "Meta"); mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl"); @@ -411,27 +416,45 @@ public final class KeyboardShortcutListSearch { mKeyCharacterMap = mBackupKeyCharacterMap; } + private boolean mAppShortcutsReceived; + private boolean mImeShortcutsReceived; + @VisibleForTesting void showKeyboardShortcuts(int deviceId) { retrieveKeyCharacterMap(deviceId); - mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() { - @Override - public void onKeyboardShortcutsReceived( - final List<KeyboardShortcutGroup> result) { - // Add specific app shortcuts - if (result.isEmpty()) { - mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false); - } else { - mSpecificAppGroup = reMapToKeyboardShortcutMultiMappingGroup(result); - mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true); - } - mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup); - mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup); - mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup); - mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup); - showKeyboardShortcutSearchList(mFullShortsGroup); + mAppShortcutsReceived = false; + mImeShortcutsReceived = false; + mWindowManager.requestAppKeyboardShortcuts(result -> { + // Add specific app shortcuts + if (result.isEmpty()) { + mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false); + } else { + mSpecificAppGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result)); + mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true); + } + mAppShortcutsReceived = true; + if (mImeShortcutsReceived) { + mergeAndShowKeyboardShortcutsGroups(); } }, deviceId); + mWindowManager.requestImeKeyboardShortcuts(result -> { + // Add specific Ime shortcuts + if (!result.isEmpty()) { + mInputGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result)); + } + mImeShortcutsReceived = true; + if (mAppShortcutsReceived) { + mergeAndShowKeyboardShortcutsGroups(); + } + }, deviceId); + } + + private void mergeAndShowKeyboardShortcutsGroups() { + mFullShortsGroup.add(SHORTCUT_SYSTEM_INDEX, mSystemGroup); + mFullShortsGroup.add(SHORTCUT_INPUT_INDEX, mInputGroup); + mFullShortsGroup.add(SHORTCUT_OPENAPPS_INDEX, mOpenAppsGroup); + mFullShortsGroup.add(SHORTCUT_SPECIFICAPP_INDEX, mSpecificAppGroup); + showKeyboardShortcutSearchList(mFullShortsGroup); } // The original data structure is only for 1-to-1 shortcut mapping, so remap the old diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java index 43fbc7cbae03..a3fd82e9b140 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java @@ -56,7 +56,6 @@ import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.WindowManager.KeyboardShortcutsReceiver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.LinearLayout; @@ -129,6 +128,9 @@ public final class KeyboardShortcuts { private KeyCharacterMap mKeyCharacterMap; private KeyCharacterMap mBackupKeyCharacterMap; + @Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null; + @Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null; + @VisibleForTesting KeyboardShortcuts(Context context, WindowManager windowManager) { this.mContext = new ContextThemeWrapper( @@ -324,6 +326,12 @@ public final class KeyboardShortcuts { mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変æ›"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変æ›"); mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "ã‹ãª"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_LEFT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_ALT_RIGHT, "Alt"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_LEFT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_CTRL_RIGHT, "Ctrl"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_LEFT, "Shift"); + mSpecialCharacterNames.put(KeyEvent.KEYCODE_SHIFT_RIGHT, "Shift"); mModifierNames.put(KeyEvent.META_META_ON, "Meta"); mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl"); @@ -382,18 +390,36 @@ public final class KeyboardShortcuts { @VisibleForTesting void showKeyboardShortcuts(int deviceId) { retrieveKeyCharacterMap(deviceId); - mWindowManager.requestAppKeyboardShortcuts(new KeyboardShortcutsReceiver() { - @Override - public void onKeyboardShortcutsReceived( - final List<KeyboardShortcutGroup> result) { - result.add(getSystemShortcuts()); - final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts(); - if (appShortcuts != null) { - result.add(appShortcuts); - } - showKeyboardShortcutsDialog(result); - } - }, deviceId); + mReceivedAppShortcutGroups = null; + mReceivedImeShortcutGroups = null; + mWindowManager.requestAppKeyboardShortcuts( + result -> { + mReceivedAppShortcutGroups = result; + maybeMergeAndShowKeyboardShortcuts(); + }, deviceId); + mWindowManager.requestImeKeyboardShortcuts( + result -> { + mReceivedImeShortcutGroups = result; + maybeMergeAndShowKeyboardShortcuts(); + }, deviceId); + } + + private void maybeMergeAndShowKeyboardShortcuts() { + if (mReceivedAppShortcutGroups == null || mReceivedImeShortcutGroups == null) { + return; + } + List<KeyboardShortcutGroup> shortcutGroups = mReceivedAppShortcutGroups; + shortcutGroups.addAll(mReceivedImeShortcutGroups); + mReceivedAppShortcutGroups = null; + mReceivedImeShortcutGroups = null; + + final KeyboardShortcutGroup defaultAppShortcuts = + getDefaultApplicationShortcuts(); + if (defaultAppShortcuts != null) { + shortcutGroups.add(defaultAppShortcuts); + } + shortcutGroups.add(getSystemShortcuts()); + showKeyboardShortcutsDialog(shortcutGroups); } private void dismissKeyboardShortcuts() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java index 109f185c625e..22c9e45d48af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutListSearchTest.java @@ -76,5 +76,6 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase { mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); + verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java index ea822aa00429..a3ecde0fe976 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsTest.java @@ -75,5 +75,6 @@ public class KeyboardShortcutsTest extends SysuiTestCase { mKeyboardShortcuts.toggle(mContext, DEVICE_ID); verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt()); + verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt()); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 7460d12d20b2..0caff466bee3 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7185,15 +7185,45 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void requestAppKeyboardShortcuts(IResultReceiver receiver, int deviceId) { - mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS, - "requestAppKeyboardShortcuts"); + enforceRegisterWindowManagerListenersPermission("requestAppKeyboardShortcuts"); + WindowState focusedWindow = getFocusedWindow(); + if (focusedWindow == null || focusedWindow.mClient == null) { + notifyReceiverWithEmptyBundle(receiver); + return; + } try { - WindowState focusedWindow = getFocusedWindow(); - if (focusedWindow != null && focusedWindow.mClient != null) { - getFocusedWindow().mClient.requestAppKeyboardShortcuts(receiver, deviceId); - } + focusedWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId); + } catch (RemoteException e) { + notifyReceiverWithEmptyBundle(receiver); + } + } + + @Override + public void requestImeKeyboardShortcuts(IResultReceiver receiver, int deviceId) { + enforceRegisterWindowManagerListenersPermission("requestImeKeyboardShortcuts"); + + WindowState imeWindow = mRoot.getCurrentInputMethodWindow(); + if (imeWindow == null || imeWindow.mClient == null) { + notifyReceiverWithEmptyBundle(receiver); + return; + } + try { + imeWindow.mClient.requestAppKeyboardShortcuts(receiver, deviceId); + } catch (RemoteException e) { + notifyReceiverWithEmptyBundle(receiver); + } + } + + private void enforceRegisterWindowManagerListenersPermission(String message) { + mContext.enforceCallingOrSelfPermission(REGISTER_WINDOW_MANAGER_LISTENERS, message); + } + + private static void notifyReceiverWithEmptyBundle(IResultReceiver receiver) { + try { + receiver.send(0, Bundle.EMPTY); } catch (RemoteException e) { + ProtoLog.e(WM_ERROR, "unable to call receiver for empty keyboard shortcuts"); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index d3f68185a269..197ee92aa7eb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -52,6 +52,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -99,6 +100,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.AdoptShellPermissionsRule; +import com.android.internal.os.IResultReceiver; import org.junit.Rule; import org.junit.Test; @@ -905,6 +907,56 @@ public class WindowManagerServiceTests extends WindowTestsBase { argThat(h -> (h.inputConfig & InputConfig.SPY) == InputConfig.SPY)); } + @Test + public void testRequestKeyboardShortcuts_noWindow() { + doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString()); + doReturn(null).when(mWm).getFocusedWindowLocked(); + doReturn(null).when(mWm.mRoot).getCurrentInputMethodWindow(); + + TestResultReceiver receiver = new TestResultReceiver(); + mWm.requestAppKeyboardShortcuts(receiver, 0); + assertNotNull(receiver.resultData); + assertTrue(receiver.resultData.isEmpty()); + + receiver = new TestResultReceiver(); + mWm.requestImeKeyboardShortcuts(receiver, 0); + assertNotNull(receiver.resultData); + assertTrue(receiver.resultData.isEmpty()); + } + + @Test + public void testRequestKeyboardShortcuts() throws RemoteException { + final IWindow window = mock(IWindow.class); + final IBinder binder = mock(IBinder.class); + doReturn(binder).when(window).asBinder(); + final WindowState windowState = + createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, "appWin", window); + doNothing().when(mWm.mContext).enforceCallingOrSelfPermission(anyString(), anyString()); + doReturn(windowState).when(mWm).getFocusedWindowLocked(); + doReturn(windowState).when(mWm.mRoot).getCurrentInputMethodWindow(); + + TestResultReceiver receiver = new TestResultReceiver(); + mWm.requestAppKeyboardShortcuts(receiver, 0); + mWm.requestImeKeyboardShortcuts(receiver, 0); + verify(window, times(2)).requestAppKeyboardShortcuts(receiver, 0); + } + + class TestResultReceiver implements IResultReceiver { + public android.os.Bundle resultData; + private final IBinder mBinder = mock(IBinder.class); + + @Override + public void send(int resultCode, android.os.Bundle resultData) + throws android.os.RemoteException { + this.resultData = resultData; + } + + @Override + public android.os.IBinder asBinder() { + return mBinder; + } + } + private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); when(remoteToken.toWindowContainerToken()).thenReturn(wct); -- GitLab