Skip to content
Snippets Groups Projects
Commit 81077e77 authored by Ben Murdoch's avatar Ben Murdoch
Browse files

Clear icons for app/ime keyboard shortcuts in shortcut helper.

If application supplied keyboard shortcuts have an icon, clear that icon
before processing it in the shortcut helper. Icons are not intended for
use by applications.

Additionally resolves a race condition between receiving IME and app
specific shortcuts, and closes the shortcut helper if it is showing
while a user switch takes place.

Bug: 331180422
Flag: com.android.systemui.validate_keyboard_shortcut_helper_icon_uri
Test: See b/331180422; atest KeyboardShortcutsTest KeyboardShortcutListSearchTest

Change-Id: Ia628886e80e956d0c423d7a7ebe67b2b8ca8c264
parent 7f1b8edc
No related branches found
No related tags found
No related merge requests found
......@@ -28,8 +28,8 @@ import android.os.Parcelable;
* Information about a Keyboard Shortcut.
*/
public final class KeyboardShortcutInfo implements Parcelable {
private final CharSequence mLabel;
private final Icon mIcon;
@Nullable private final CharSequence mLabel;
@Nullable private Icon mIcon;
private final char mBaseCharacter;
private final int mKeycode;
private final int mModifiers;
......@@ -115,6 +115,15 @@ public final class KeyboardShortcutInfo implements Parcelable {
return mIcon;
}
/**
* Removes an icon that was previously set.
*
* @hide
*/
public void clearIcon() {
mIcon = null;
}
/**
* Returns the base keycode that, combined with the modifiers, triggers this shortcut. If the
* base character was set instead, returns {@link KeyEvent#KEYCODE_UNKNOWN}. Valid keycodes are
......@@ -165,4 +174,4 @@ public final class KeyboardShortcutInfo implements Parcelable {
return new KeyboardShortcutInfo[size];
}
};
}
\ No newline at end of file
}
......@@ -19,9 +19,14 @@ package com.android.systemui.statusbar;
import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.SynchronousUserSwitchObserver;
import android.app.UserSwitchObserver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
......@@ -37,6 +42,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.hardware.input.InputManagerGlobal;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.text.Editable;
......@@ -136,6 +142,8 @@ public final class KeyboardShortcutListSearch {
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final HandlerThread mHandlerThread = new HandlerThread("KeyboardShortcutHelper");
@VisibleForTesting Handler mBackgroundHandler;
@VisibleForTesting public Context mContext;
private final IPackageManager mPackageManager;
......@@ -143,6 +151,13 @@ public final class KeyboardShortcutListSearch {
private KeyCharacterMap mKeyCharacterMap;
private KeyCharacterMap mBackupKeyCharacterMap;
private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() {
@Override
public void onUserSwitching(int newUserId) throws RemoteException {
dismiss();
}
};
@VisibleForTesting
KeyboardShortcutListSearch(Context context, WindowManager windowManager) {
this.mContext = new ContextThemeWrapper(
......@@ -413,36 +428,75 @@ public final class KeyboardShortcutListSearch {
private boolean mAppShortcutsReceived;
private boolean mImeShortcutsReceived;
@VisibleForTesting
public void showKeyboardShortcuts(int deviceId) {
retrieveKeyCharacterMap(deviceId);
mAppShortcutsReceived = false;
mImeShortcutsReceived = false;
mWindowManager.requestAppKeyboardShortcuts(result -> {
// Add specific app shortcuts
private void onAppSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
// Add specific app shortcuts
if (result != null) {
if (result.isEmpty()) {
mCurrentAppPackageName = null;
mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, false);
} else {
mCurrentAppPackageName = result.get(0).getPackageName();
mSpecificAppGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
if (validateKeyboardShortcutHelperIconUri()) {
KeyboardShortcuts.sanitiseShortcuts(result);
}
mSpecificAppGroup.addAll(
reMapToKeyboardShortcutMultiMappingGroup(result));
mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
}
mAppShortcutsReceived = true;
if (mImeShortcutsReceived) {
mergeAndShowKeyboardShortcutsGroups();
}
}, deviceId);
mWindowManager.requestImeKeyboardShortcuts(result -> {
// Add specific Ime shortcuts
}
mAppShortcutsReceived = true;
if (mImeShortcutsReceived) {
mergeAndShowKeyboardShortcutsGroups();
}
}
private void onImeSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
// Add specific Ime shortcuts
if (result != null) {
if (!result.isEmpty()) {
mInputGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
if (validateKeyboardShortcutHelperIconUri()) {
KeyboardShortcuts.sanitiseShortcuts(result);
}
mInputGroup.addAll(
reMapToKeyboardShortcutMultiMappingGroup(result));
}
mImeShortcutsReceived = true;
if (mAppShortcutsReceived) {
mergeAndShowKeyboardShortcutsGroups();
}
mImeShortcutsReceived = true;
if (mAppShortcutsReceived) {
mergeAndShowKeyboardShortcutsGroups();
}
}
@VisibleForTesting
public void showKeyboardShortcuts(int deviceId) {
if (mBackgroundHandler == null) {
mHandlerThread.start();
mBackgroundHandler = new Handler(mHandlerThread.getLooper());
}
if (validateKeyboardShortcutHelperIconUri()) {
try {
ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
} catch (RemoteException e) {
Log.e(TAG, "could not register user switch observer", e);
}
}, deviceId);
}
retrieveKeyCharacterMap(deviceId);
mAppShortcutsReceived = false;
mImeShortcutsReceived = false;
mWindowManager.requestAppKeyboardShortcuts(
result -> {
mBackgroundHandler.post(() -> {
onAppSpecificShortcutsReceived(result);
});
}, deviceId);
mWindowManager.requestImeKeyboardShortcuts(
result -> {
mBackgroundHandler.post(() -> {
onImeSpecificShortcutsReceived(result);
});
}, deviceId);
}
private void mergeAndShowKeyboardShortcutsGroups() {
......@@ -508,6 +562,14 @@ public final class KeyboardShortcutListSearch {
mKeyboardShortcutsBottomSheetDialog.dismiss();
mKeyboardShortcutsBottomSheetDialog = null;
}
mHandlerThread.quit();
if (validateKeyboardShortcutHelperIconUri()) {
try {
ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
} catch (RemoteException e) {
Log.e(TAG, "Could not unregister user switch observer", e);
}
}
}
private KeyboardShortcutMultiMappingGroup getMultiMappingSystemShortcuts(Context context) {
......
......@@ -20,11 +20,16 @@ import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
import static com.android.systemui.Flags.validateKeyboardShortcutHelperIconUri;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.AppGlobals;
import android.app.Dialog;
import android.app.SynchronousUserSwitchObserver;
import android.app.UserSwitchObserver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
......@@ -39,6 +44,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
......@@ -93,6 +99,8 @@ public final class KeyboardShortcuts {
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final HandlerThread mHandlerThread = new HandlerThread("KeyboardShortcutHelper");
@VisibleForTesting Handler mBackgroundHandler;
@VisibleForTesting public Context mContext;
private final IPackageManager mPackageManager;
private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
......@@ -129,6 +137,13 @@ public final class KeyboardShortcuts {
@Nullable private List<KeyboardShortcutGroup> mReceivedAppShortcutGroups = null;
@Nullable private List<KeyboardShortcutGroup> mReceivedImeShortcutGroups = null;
private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() {
@Override
public void onUserSwitching(int newUserId) throws RemoteException {
dismiss();
}
};
@VisibleForTesting
KeyboardShortcuts(Context context, WindowManager windowManager) {
this.mContext = new ContextThemeWrapper(
......@@ -374,21 +389,68 @@ public final class KeyboardShortcuts {
@VisibleForTesting
public void showKeyboardShortcuts(int deviceId) {
if (mBackgroundHandler == null) {
mHandlerThread.start();
mBackgroundHandler = new Handler(mHandlerThread.getLooper());
}
if (validateKeyboardShortcutHelperIconUri()) {
try {
ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
} catch (RemoteException e) {
Log.e(TAG, "could not register user switch observer", e);
}
}
retrieveKeyCharacterMap(deviceId);
mReceivedAppShortcutGroups = null;
mReceivedImeShortcutGroups = null;
mWindowManager.requestAppKeyboardShortcuts(
result -> {
mReceivedAppShortcutGroups = result;
maybeMergeAndShowKeyboardShortcuts();
mBackgroundHandler.post(() -> {
onAppSpecificShortcutsReceived(result);
});
}, deviceId);
mWindowManager.requestImeKeyboardShortcuts(
result -> {
mReceivedImeShortcutGroups = result;
maybeMergeAndShowKeyboardShortcuts();
mBackgroundHandler.post(() -> {
onImeSpecificShortcutsReceived(result);
});
}, deviceId);
}
private void onAppSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
mReceivedAppShortcutGroups =
result == null ? Collections.emptyList() : result;
if (validateKeyboardShortcutHelperIconUri()) {
sanitiseShortcuts(mReceivedAppShortcutGroups);
}
maybeMergeAndShowKeyboardShortcuts();
}
private void onImeSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
mReceivedImeShortcutGroups =
result == null ? Collections.emptyList() : result;
if (validateKeyboardShortcutHelperIconUri()) {
sanitiseShortcuts(mReceivedImeShortcutGroups);
}
maybeMergeAndShowKeyboardShortcuts();
}
static void sanitiseShortcuts(List<KeyboardShortcutGroup> shortcutGroups) {
for (KeyboardShortcutGroup group : shortcutGroups) {
for (KeyboardShortcutInfo info : group.getItems()) {
info.clearIcon();
}
}
}
private void maybeMergeAndShowKeyboardShortcuts() {
if (mReceivedAppShortcutGroups == null || mReceivedImeShortcutGroups == null) {
return;
......@@ -413,6 +475,14 @@ public final class KeyboardShortcuts {
mKeyboardShortcutsDialog.dismiss();
mKeyboardShortcutsDialog = null;
}
mHandlerThread.quit();
if (validateKeyboardShortcutHelperIconUri()) {
try {
ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
} catch (RemoteException e) {
Log.e(TAG, "Could not unregister user switch observer", e);
}
}
}
private KeyboardShortcutGroup getSystemShortcuts() {
......
......@@ -20,14 +20,21 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.platform.test.annotations.EnableFlags;
import android.view.KeyboardShortcutGroup;
import android.view.KeyboardShortcutInfo;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.google.android.material.bottomsheet.BottomSheetDialog;
......@@ -36,10 +43,14 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.Arrays;
import java.util.Collections;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class KeyboardShortcutListSearchTest extends SysuiTestCase {
......@@ -51,6 +62,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase {
@Mock private BottomSheetDialog mBottomSheetDialog;
@Mock WindowManager mWindowManager;
@Mock Handler mHandler;
@Before
public void setUp() {
......@@ -58,6 +70,7 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase {
mKeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
mKeyboardShortcutListSearch.mKeyboardShortcutsBottomSheetDialog = mBottomSheetDialog;
mKeyboardShortcutListSearch.mContext = mContext;
mKeyboardShortcutListSearch.mBackgroundHandler = mHandler;
}
@Test
......@@ -78,4 +91,59 @@ public class KeyboardShortcutListSearchTest extends SysuiTestCase {
verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt());
}
@Test
@EnableFlags(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI)
public void requestAppKeyboardShortcuts_callback_sanitisesIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID);
ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor =
ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class);
ArgumentCaptor<Runnable> handlerRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mWindowManager).requestAppKeyboardShortcuts(callbackCaptor.capture(), anyInt());
callbackCaptor.getValue().onKeyboardShortcutsReceived(Collections.singletonList(group));
verify(mHandler).post(handlerRunnableCaptor.capture());
handlerRunnableCaptor.getValue().run();
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
@Test
@EnableFlags(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI)
public void requestImeKeyboardShortcuts_callback_sanitisesIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
mKeyboardShortcutListSearch.toggle(mContext, DEVICE_ID);
ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor =
ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class);
ArgumentCaptor<Runnable> handlerRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mWindowManager).requestImeKeyboardShortcuts(callbackCaptor.capture(), anyInt());
callbackCaptor.getValue().onKeyboardShortcutsReceived(Collections.singletonList(group));
verify(mHandler).post(handlerRunnableCaptor.capture());
handlerRunnableCaptor.getValue().run();
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
private KeyboardShortcutGroup createKeyboardShortcutGroupForIconTests() {
Icon icon = mock(Icon.class);
KeyboardShortcutInfo info1 = mock(KeyboardShortcutInfo.class);
KeyboardShortcutInfo info2 = mock(KeyboardShortcutInfo.class);
when(info1.getIcon()).thenReturn(icon);
when(info2.getIcon()).thenReturn(icon);
when(info1.getLabel()).thenReturn("label");
when(info2.getLabel()).thenReturn("label");
KeyboardShortcutGroup group = new KeyboardShortcutGroup("label",
Arrays.asList(new KeyboardShortcutInfo[]{ info1, info2}));
group.setPackageName("com.example");
return group;
}
}
......@@ -20,25 +20,36 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Dialog;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.platform.test.annotations.EnableFlags;
import android.view.KeyboardShortcutGroup;
import android.view.KeyboardShortcutInfo;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.Arrays;
import java.util.Collections;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class KeyboardShortcutsTest extends SysuiTestCase {
......@@ -50,6 +61,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase {
@Mock private Dialog mDialog;
@Mock WindowManager mWindowManager;
@Mock Handler mHandler;
@Before
public void setUp() {
......@@ -57,6 +69,7 @@ public class KeyboardShortcutsTest extends SysuiTestCase {
mKeyboardShortcuts.sInstance = mKeyboardShortcuts;
mKeyboardShortcuts.mKeyboardShortcutsDialog = mDialog;
mKeyboardShortcuts.mContext = mContext;
mKeyboardShortcuts.mBackgroundHandler = mHandler;
}
@Test
......@@ -77,4 +90,78 @@ public class KeyboardShortcutsTest extends SysuiTestCase {
verify(mWindowManager).requestAppKeyboardShortcuts(any(), anyInt());
verify(mWindowManager).requestImeKeyboardShortcuts(any(), anyInt());
}
@Test
public void sanitiseShortcuts_clearsIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
KeyboardShortcuts.sanitiseShortcuts(Collections.singletonList(group));
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
@Test
public void sanitiseShortcuts_nullPackage_clearsIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
group.setPackageName(null);
KeyboardShortcuts.sanitiseShortcuts(Collections.singletonList(group));
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
@Test
@EnableFlags(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI)
public void requestAppKeyboardShortcuts_callback_sanitisesIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
mKeyboardShortcuts.toggle(mContext, DEVICE_ID);
ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor =
ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class);
ArgumentCaptor<Runnable> handlerRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mWindowManager).requestAppKeyboardShortcuts(callbackCaptor.capture(), anyInt());
callbackCaptor.getValue().onKeyboardShortcutsReceived(Collections.singletonList(group));
verify(mHandler).post(handlerRunnableCaptor.capture());
handlerRunnableCaptor.getValue().run();
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
@Test
@EnableFlags(Flags.FLAG_VALIDATE_KEYBOARD_SHORTCUT_HELPER_ICON_URI)
public void requestImeKeyboardShortcuts_callback_sanitisesIcons() {
KeyboardShortcutGroup group = createKeyboardShortcutGroupForIconTests();
mKeyboardShortcuts.toggle(mContext, DEVICE_ID);
ArgumentCaptor<WindowManager.KeyboardShortcutsReceiver> callbackCaptor =
ArgumentCaptor.forClass(WindowManager.KeyboardShortcutsReceiver.class);
ArgumentCaptor<Runnable> handlerRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mWindowManager).requestImeKeyboardShortcuts(callbackCaptor.capture(), anyInt());
callbackCaptor.getValue().onKeyboardShortcutsReceived(Collections.singletonList(group));
verify(mHandler).post(handlerRunnableCaptor.capture());
handlerRunnableCaptor.getValue().run();
verify(group.getItems().get(0)).clearIcon();
verify(group.getItems().get(1)).clearIcon();
}
private KeyboardShortcutGroup createKeyboardShortcutGroupForIconTests() {
Icon icon = mock(Icon.class);
KeyboardShortcutInfo info1 = mock(KeyboardShortcutInfo.class);
KeyboardShortcutInfo info2 = mock(KeyboardShortcutInfo.class);
when(info1.getIcon()).thenReturn(icon);
when(info2.getIcon()).thenReturn(icon);
KeyboardShortcutGroup group = new KeyboardShortcutGroup("label",
Arrays.asList(new KeyboardShortcutInfo[]{ info1, info2}));
group.setPackageName("com.example");
return group;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment