Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 2744c92a authored by Ben Murdoch's avatar Ben Murdoch Committed by Michael Bestas
Browse files

BACKPORT: 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
Test: See b/331180422; atest KeyboardShortcutsTest KeyboardShortcutListSearchTest

Change-Id: Ia628886e80e956d0c423d7a7ebe67b2b8ca8c264
parent 57d008fd
Loading
Loading
Loading
Loading
+12 −3
Original line number Diff line number Diff line
@@ -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
+73 −21
Original line number Diff line number Diff line
@@ -21,7 +21,10 @@ import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;

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 +40,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;
@@ -132,6 +136,8 @@ public final class KeyboardShortcutListSearch {
    };

    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final HandlerThread mHandlerThread = new HandlerThread("KeyboardShortcutHelper");
    @VisibleForTesting Handler mBackgroundHandler;
    @VisibleForTesting Context mContext;
    private final IPackageManager mPackageManager;

@@ -139,6 +145,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(
@@ -409,35 +422,68 @@ public final class KeyboardShortcutListSearch {
    private boolean mAppShortcutsReceived;
    private boolean mImeShortcutsReceived;

    @VisibleForTesting
    void showKeyboardShortcuts(int deviceId) {
        retrieveKeyCharacterMap(deviceId);
        mAppShortcutsReceived = false;
        mImeShortcutsReceived = false;
        mWindowManager.requestAppKeyboardShortcuts(result -> {
    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));
                KeyboardShortcuts.sanitiseShortcuts(result);
                mSpecificAppGroup.addAll(
                        reMapToKeyboardShortcutMultiMappingGroup(result));
                mKeySearchResultMap.put(SHORTCUT_SPECIFICAPP_INDEX, true);
            }
        }
        mAppShortcutsReceived = true;
        if (mImeShortcutsReceived) {
            mergeAndShowKeyboardShortcutsGroups();
        }
        }, deviceId);
        mWindowManager.requestImeKeyboardShortcuts(result -> {
    }

    private void onImeSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
        // Add specific Ime shortcuts
        if (result != null) {
            if (!result.isEmpty()) {
                mInputGroup.addAll(reMapToKeyboardShortcutMultiMappingGroup(result));
                KeyboardShortcuts.sanitiseShortcuts(result);
                mInputGroup.addAll(
                        reMapToKeyboardShortcutMultiMappingGroup(result));
            }
        }
        mImeShortcutsReceived = true;
        if (mAppShortcutsReceived) {
            mergeAndShowKeyboardShortcutsGroups();
        }
    }

    @VisibleForTesting
    void showKeyboardShortcuts(int deviceId) {
        if (mBackgroundHandler == null) {
            mHandlerThread.start();
            mBackgroundHandler = new Handler(mHandlerThread.getLooper());
        }

        try {
            ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
        } catch (RemoteException e) {
            Log.e(TAG, "could not register user switch observer", e);
        }

        retrieveKeyCharacterMap(deviceId);
        mAppShortcutsReceived = false;
        mImeShortcutsReceived = false;
        mWindowManager.requestAppKeyboardShortcuts(
                result -> {
                    mBackgroundHandler.post(() -> {
                        onAppSpecificShortcutsReceived(result);
                    });
                }, deviceId);
        mWindowManager.requestImeKeyboardShortcuts(
                result -> {
                    mBackgroundHandler.post(() -> {
                        onImeSpecificShortcutsReceived(result);
                    });
                }, deviceId);
    }

@@ -474,6 +520,12 @@ public final class KeyboardShortcutListSearch {
            mKeyboardShortcutsBottomSheetDialog.dismiss();
            mKeyboardShortcutsBottomSheetDialog = null;
        }
        mHandlerThread.quit();
        try {
            ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
        } catch (RemoteException e) {
            Log.e(TAG, "Could not unregister user switch observer", e);
        }
    }

    private KeyboardShortcutMultiMappingGroup getMultiMappingSystemShortcuts(Context context) {
+64 −4
Original line number Diff line number Diff line
@@ -22,9 +22,12 @@ import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;

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;
@@ -40,6 +43,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;
@@ -94,6 +98,8 @@ public final class KeyboardShortcuts {
    };

    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final HandlerThread mHandlerThread = new HandlerThread("KeyboardShortcutHelper");
    @VisibleForTesting Handler mBackgroundHandler;
    @VisibleForTesting Context mContext;
    private final IPackageManager mPackageManager;
    private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
@@ -130,6 +136,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(
@@ -375,21 +388,62 @@ public final class KeyboardShortcuts {

    @VisibleForTesting
    void showKeyboardShortcuts(int deviceId) {
        if (mBackgroundHandler == null) {
            mHandlerThread.start();
            mBackgroundHandler = new Handler(mHandlerThread.getLooper());
        }

        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;

        sanitiseShortcuts(mReceivedAppShortcutGroups);

        maybeMergeAndShowKeyboardShortcuts();
    }

    private void onImeSpecificShortcutsReceived(List<KeyboardShortcutGroup> result) {
        mReceivedImeShortcutGroups =
                result == null ? Collections.emptyList() : result;

        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 +467,12 @@ public final class KeyboardShortcuts {
            mKeyboardShortcutsDialog.dismiss();
            mKeyboardShortcutsDialog = null;
        }
        mHandlerThread.quit();
        try {
            ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
        } catch (RemoteException e) {
            Log.e(TAG, "Could not unregister user switch observer", e);
        }
    }

    private KeyboardShortcutGroup getSystemShortcuts() {
+68 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+87 −0
Original line number Diff line number Diff line
@@ -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;
    }
}