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

Commit 61e1f831 authored by Cosmin Băieș's avatar Cosmin Băieș
Browse files

Add IME switch button long click support

This adds support for handling long clicking on the IME switch button.
Initially it will behave exactly like short clicking, i.e. show the IME
switcher menu.

As the IME navigation bar didn't previously support long clicks, this
follows along the logic of the SystemUI navigation bar, with a small
modification: holding down the button for longer than the long click
timeout does not send a cancel event, to maintain the current behaviour
that holding down the IME dismiss button should still hide the IME.

Flag: android.view.inputmethod.ime_switcher_revamp
Test: atest
  NavigationBarTest#testImeSwitcherClick
  NavigationBarTest#testImeSwitcherLongClick
  InputMethodServiceTest#testBackButtonClick
  InputMethodServiceTest#testBackButtonLongClick
  InputMethodServiceTest#testImeSwitchButtonClick
  InputMethodServiceTest#testImeSwitchButtonLongClick
Bug: 311791923
Change-Id: I633da5b03fa50ec1ccf515a4df5538e397d15442
parent 0d210c3e
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -101,6 +101,7 @@ import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.InputType;
import android.text.Layout;
@@ -4340,6 +4341,16 @@ public class InputMethodService extends AbstractInputMethodService {
        return mNavigationBarController.isShown();
    }

    /**
     * Called when the IME switch button was clicked from the client. This will show the input
     * method picker dialog.
     *
     * @hide
     */
    final void onImeSwitchButtonClickFromClient() {
        mPrivOps.onImeSwitchButtonClickFromClient(getDisplayId(), UserHandle.myUserId());
    }

    /**
     * Used to inject custom {@link InputMethodServiceInternal}.
     *
+15 −1
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import android.view.WindowInsets;
import android.view.WindowInsetsController.Appearance;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;

import com.android.internal.inputmethod.InputMethodNavButtonFlags;
@@ -145,7 +146,8 @@ final class NavigationBarController {
        return mImpl.toDebugString();
    }

    private static final class Impl implements Callback, Window.DecorCallback {
    private static final class Impl implements Callback, Window.DecorCallback,
            NavigationBarView.ButtonClickListener {
        private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700;

        // Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE
@@ -241,6 +243,7 @@ final class NavigationBarController {
                                    ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN
                                    : 0);
                    navigationBarView.setNavigationIconHints(hints);
                    navigationBarView.prepareNavButtons(this);
                }
            } else {
                mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
@@ -592,6 +595,17 @@ final class NavigationBarController {
            return drawLegacyNavigationBarBackground;
        }

        @Override
        public void onImeSwitchButtonClick(View v) {
            mService.onImeSwitchButtonClickFromClient();
        }

        @Override
        public boolean onImeSwitchButtonLongClick(View v) {
            v.getContext().getSystemService(InputMethodManager.class).showInputMethodPicker();
            return true;
        }

        /**
         * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead.
         */
+35 −2
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.Flags;
import android.view.inputmethod.InputConnection;
import android.widget.ImageView;

@@ -58,12 +59,30 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
    private int mTouchDownY;
    private AudioManager mAudioManager;
    private boolean mGestureAborted;
    /**
     * Whether the long click action has been invoked. The short click action is invoked on the up
     * event while a long click is invoked as soon as the long press duration is reached, so a long
     * click could be performed before the short click is checked, in which case the short click's
     * action should not be invoked.
     *
     * @see View#mHasPerformedLongPress
     */
    private boolean mLongClicked;
    private OnClickListener mOnClickListener;
    private final KeyButtonRipple mRipple;
    private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    private float mDarkIntensity;
    private boolean mHasOvalBg = false;

    /** Runnable for checking whether the long click action should be performed. */
    private final Runnable mCheckLongPress = new Runnable() {
        public void run() {
            if (isPressed() && performLongClick()) {
                mLongClicked = true;
            }
        }
    };

    public KeyButtonView(Context context, AttributeSet attrs) {
        super(context, attrs);

@@ -159,6 +178,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = SystemClock.uptimeMillis();
                mLongClicked = false;
                setPressed(true);

                // Use raw X and Y to detect gestures in case a parent changes the x and y values
@@ -173,6 +193,10 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
                if (!showSwipeUI) {
                    playSoundEffect(SoundEffectConstants.CLICK);
                }
                if (Flags.imeSwitcherRevamp() && isLongClickable()) {
                    removeCallbacks(mCheckLongPress);
                    postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
                }
                break;
            case MotionEvent.ACTION_MOVE:
                x = (int) ev.getRawX();
@@ -183,6 +207,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
                    // When quick step is enabled, prevent animating the ripple triggered by
                    // setPressed and decide to run it on touch up
                    setPressed(false);
                    if (isLongClickable()) {
                        removeCallbacks(mCheckLongPress);
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
@@ -190,9 +217,12 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
                if (mCode != KEYCODE_UNKNOWN) {
                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                }
                if (isLongClickable()) {
                    removeCallbacks(mCheckLongPress);
                }
                break;
            case MotionEvent.ACTION_UP:
                final boolean doIt = isPressed();
                final boolean doIt = isPressed() && !mLongClicked;
                setPressed(false);
                final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
                if (showSwipeUI) {
@@ -201,7 +231,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
                        performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                        playSoundEffect(SoundEffectConstants.CLICK);
                    }
                } else if (doHapticFeedback) {
                } else if (doHapticFeedback && !mLongClicked) {
                    // Always send a release ourselves because it doesn't seem to be sent elsewhere
                    // and it feels weird to sometimes get a release haptic and other times not.
                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
@@ -221,6 +251,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                    }
                }
                if (isLongClickable()) {
                    removeCallbacks(mCheckLongPress);
                }
                break;
        }

+42 −4
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.DrawableRes;
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.app.StatusBarManager;
import android.content.Context;
import android.content.res.Configuration;
@@ -39,6 +40,7 @@ import android.view.Surface;
import android.view.View;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;

@@ -79,6 +81,28 @@ public final class NavigationBarView extends FrameLayout {

    private NavigationBarInflaterView mNavigationInflaterView;

    /**
     * Interface definition for callbacks to be invoked when navigation bar buttons are clicked.
     */
    public interface ButtonClickListener {

        /**
         * Called when the IME switch button is clicked.
         *
         * @param v The view that was clicked.
         */
        void onImeSwitchButtonClick(View v);

        /**
         * Called when the IME switch button has been clicked and held.
         *
         * @param v The view that was clicked and held.
         *
         * @return true if the callback consumed the long click, false otherwise.
         */
        boolean onImeSwitchButtonLongClick(View v);
    }

    public NavigationBarView(Context context, AttributeSet attrs) {
        super(context, attrs);

@@ -98,14 +122,28 @@ public final class NavigationBarView extends FrameLayout {
                new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle));

        mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this);
    }

    /**
     * Prepares the navigation bar buttons to be used and sets the on click listeners.
     *
     * @param listener The listener used to handle the clicks on the navigation bar buttons.
     */
    public void prepareNavButtons(@NonNull ButtonClickListener listener) {
        getBackButton().setLongClickable(false);

        if (Flags.imeSwitcherRevamp()) {
            final var imeSwitchButton = getImeSwitchButton();
            imeSwitchButton.setLongClickable(true);
            imeSwitchButton.setOnClickListener(listener::onImeSwitchButtonClick);
            imeSwitchButton.setOnLongClickListener(listener::onImeSwitchButtonLongClick);
        } else {
            final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
            imeSwitchButton.setLongClickable(false);
            imeSwitchButton.setOnClickListener(view -> view.getContext()
                    .getSystemService(InputMethodManager.class).showInputMethodPicker());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
+14 −0
Original line number Diff line number Diff line
@@ -464,6 +464,20 @@ final class IInputMethodManagerGlobalInvoker {
        }
    }

    @AnyThread
    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
    static void onImeSwitchButtonClickFromSystem(int displayId) {
        final IInputMethodManager service = getService();
        if (service == null) {
            return;
        }
        try {
            service.onImeSwitchButtonClickFromSystem(displayId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @AnyThread
    @Nullable
    @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
Loading