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

Commit ff7b453c authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Minimum impl of nav buttons rendered by IME

This is the minimum implementation of rendering the back button and
the IME switcher button in the InputMethodService process.

Note that the feature is still disabled by default behind the flag.
Newly added UI logic should be running only when the feature flag is
enabled.

 Bug 215545985: Enable canImeRenderGesturalNavButtons() by default

Here is the list of known blockers before enabling the flag.

 * Bug 215552600: Subscribe gesture mode change.
 * Bug 215551357: Show IME swicher button only when necessary.
 * Bug 215549533: Support light navigation bar.
 * Bug 215550296: Support floating IME.
 * Bug 215554582: Set "input_method_rounded_corner_content_padding"
                  for each devices.

Also, after enabling the flag by default, we start cleaning up newly
added code under

  android.inputmethodservice.navigationbar.

at Bug 215443343.

Bug: 205803355
Test: Manually done as follows
  1. Build aosp_coral-userdebug and flash it
  2. Switch to gestural navigation mode.
  3. adb root
  4. adb shell setprop \
       persist.sys.ime.can_render_gestural_nav_buttons true
  5. adb reboot
  6. Tap any edit field to show AOSP Keyboard.
  7. Confirm that the back button on the navbar works.
  8. Confirm that the IME switcher button on the navbar works.
Change-Id: I3e7e1f83554444131e2765dc159617bb9e2337c7
parent 20ba147a
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -469,6 +469,10 @@ public class InputMethodService extends AbstractInputMethodService {
    InputMethodManager mImm;
    private InputMethodPrivilegedOperations mPrivOps = new InputMethodPrivilegedOperations();

    @NonNull
    private final NavigationBarController mNavigationBarController =
            new NavigationBarController(this);

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    int mTheme = 0;

@@ -611,6 +615,7 @@ public class InputMethodService extends AbstractInputMethodService {
            info.touchableRegion.set(mTmpInsets.touchableRegion);
            info.setTouchableInsets(mTmpInsets.touchableInsets);
        }
        mNavigationBarController.updateTouchableInsets(mTmpInsets, info);

        if (mInputFrame != null) {
            setImeExclusionRect(mTmpInsets.visibleTopInsets);
@@ -1534,6 +1539,7 @@ public class InputMethodService extends AbstractInputMethodService {
        mCandidatesVisibility = getCandidatesHiddenVisibility();
        mCandidatesFrame.setVisibility(mCandidatesVisibility);
        mInputFrame.setVisibility(View.GONE);
        mNavigationBarController.onViewInitialized();
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    }

@@ -1543,6 +1549,7 @@ public class InputMethodService extends AbstractInputMethodService {
        mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
                mInsetsComputer);
        doFinishInput();
        mNavigationBarController.onDestroy();
        mWindow.dismissForDestroyIfNecessary();
        if (mSettingsObserver != null) {
            mSettingsObserver.unregister();
@@ -2451,6 +2458,7 @@ public class InputMethodService extends AbstractInputMethodService {
            setImeWindowStatus(nextImeWindowStatus, mBackDisposition);
        }

        mNavigationBarController.onWindowShown();
        // compute visibility
        onWindowShown();
        mWindowVisible = true;
@@ -3656,6 +3664,7 @@ public class InputMethodService extends AbstractInputMethodService {
                + " touchableInsets=" + mTmpInsets.touchableInsets
                + " touchableRegion=" + mTmpInsets.touchableRegion);
        p.println(" mSettingsObserver=" + mSettingsObserver);
        p.println(" mNavigationBarController=" + mNavigationBarController.toDebugString());
    }

    private final ImeTracing.ServiceDumper mDumper = new ImeTracing.ServiceDumper() {
+328 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.inputmethodservice;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.StatusBarManager;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
import android.inputmethodservice.navigationbar.NavigationBarFrame;
import android.inputmethodservice.navigationbar.NavigationBarView;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManagerPolicyConstants;
import android.widget.FrameLayout;

import java.util.Objects;

/**
 * This class hides details behind {@link InputMethodService#canImeRenderGesturalNavButtons()} from
 * {@link InputMethodService}.
 *
 * <p>All the package-private methods are no-op when
 * {@link InputMethodService#canImeRenderGesturalNavButtons()} returns {@code false}.</p>
 */
final class NavigationBarController {

    private interface Callback {
        default void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
                @NonNull ViewTreeObserver.InternalInsetsInfo dest) {
        }

        default void onViewInitialized() {
        }

        default void onWindowShown() {
        }

        default void onDestroy() {
        }

        default String toDebugString() {
            return "No-op implementation";
        }

        Callback NOOP = new Callback() {
        };
    }

    private final Callback mImpl;

    NavigationBarController(@NonNull InputMethodService inputMethodService) {
        mImpl = InputMethodService.canImeRenderGesturalNavButtons()
                ? new Impl(inputMethodService) : Callback.NOOP;
    }

    void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
            @NonNull ViewTreeObserver.InternalInsetsInfo dest) {
        mImpl.updateTouchableInsets(originalInsets, dest);
    }

    void onViewInitialized() {
        mImpl.onViewInitialized();
    }

    void onWindowShown() {
        mImpl.onWindowShown();
    }

    void onDestroy() {
        mImpl.onDestroy();
    }

    String toDebugString() {
        return mImpl.toDebugString();
    }

    private static final class Impl implements Callback {
        @NonNull
        private final InputMethodService mService;

        private boolean mDestroyed = false;

        private boolean mRenderGesturalNavButtons;

        @Nullable
        private NavigationBarFrame mNavigationBarFrame;
        @Nullable
        Insets mLastInsets;

        Impl(@NonNull InputMethodService inputMethodService) {
            mService = inputMethodService;
        }

        @Nullable
        private Insets getSystemInsets() {
            if (mService.mWindow == null) {
                return null;
            }
            final View decorView = mService.mWindow.getWindow().getDecorView();
            if (decorView == null) {
                return null;
            }
            final WindowInsets windowInsets = decorView.getRootWindowInsets();
            if (windowInsets == null) {
                return null;
            }
            final Insets stableBarInsets =
                    windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
            return Insets.min(windowInsets.getInsets(WindowInsets.Type.systemBars()
                    | WindowInsets.Type.displayCutout()), stableBarInsets);
        }

        private void installNavigationBarFrameIfNecessary() {
            if (!mRenderGesturalNavButtons) {
                return;
            }
            final View rawDecorView = mService.mWindow.getWindow().getDecorView();
            if (!(rawDecorView instanceof ViewGroup)) {
                return;
            }
            final ViewGroup decorView = (ViewGroup) rawDecorView;
            mNavigationBarFrame = decorView.findViewByPredicate(
                    NavigationBarFrame.class::isInstance);
            final Insets systemInsets = getSystemInsets();
            if (mNavigationBarFrame == null) {
                mNavigationBarFrame = new NavigationBarFrame(mService);
                LayoutInflater.from(mService).inflate(
                        com.android.internal.R.layout.input_method_navigation_bar,
                        mNavigationBarFrame);
                if (systemInsets != null) {
                    decorView.addView(mNavigationBarFrame, new FrameLayout.LayoutParams(
                            ViewGroup.LayoutParams.MATCH_PARENT,
                            systemInsets.bottom, Gravity.BOTTOM));
                    mLastInsets = systemInsets;
                } else {
                    decorView.addView(mNavigationBarFrame);
                }
                final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate(
                        NavigationBarView.class::isInstance);
                if (navigationBarView != null) {
                    // TODO(b/213337792): Support InputMethodService#setBackDisposition().
                    // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary.
                    final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT
                            | StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
                    navigationBarView.setNavigationIconHints(hints);
                }
            } else {
                mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, systemInsets.bottom, Gravity.BOTTOM));
                mLastInsets = systemInsets;
            }

            mNavigationBarFrame.setBackground(null);
        }

        @Override
        public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
                @NonNull ViewTreeObserver.InternalInsetsInfo dest) {
            if (!mRenderGesturalNavButtons || mNavigationBarFrame == null
                    || mService.isExtractViewShown()) {
                return;
            }

            final Insets systemInsets = getSystemInsets();
            if (systemInsets != null) {
                final Window window = mService.mWindow.getWindow();
                final View decor = window.getDecorView();
                Region touchableRegion = null;
                final View inputFrame = mService.mInputFrame;
                switch (originalInsets.touchableInsets) {
                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
                        if (inputFrame.getVisibility() == View.VISIBLE) {
                            touchableRegion = new Region(inputFrame.getLeft(),
                                    inputFrame.getTop(), inputFrame.getRight(),
                                    inputFrame.getBottom());
                        }
                        break;
                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
                        if (inputFrame.getVisibility() == View.VISIBLE) {
                            touchableRegion = new Region(inputFrame.getLeft(),
                                    originalInsets.contentTopInsets, inputFrame.getRight(),
                                    inputFrame.getBottom());
                        }
                        break;
                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
                        if (inputFrame.getVisibility() == View.VISIBLE) {
                            touchableRegion = new Region(inputFrame.getLeft(),
                                    originalInsets.visibleTopInsets, inputFrame.getRight(),
                                    inputFrame.getBottom());
                        }
                        break;
                    case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
                        touchableRegion = new Region();
                        touchableRegion.set(originalInsets.touchableRegion);
                        break;
                }
                final Rect navBarRect = new Rect(decor.getLeft(),
                        decor.getBottom() - systemInsets.bottom,
                        decor.getRight(), decor.getBottom());
                if (touchableRegion == null) {
                    touchableRegion = new Region(navBarRect);
                } else {
                    touchableRegion.union(navBarRect);
                }

                dest.touchableRegion.set(touchableRegion);
                dest.setTouchableInsets(
                        ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);

                // TODO(b/205803355): See if we can use View#OnLayoutChangeListener().
                // TODO(b/205803355): See if we can replace DecorView#mNavigationColorViewState.view
                boolean zOrderChanged = false;
                if (decor instanceof ViewGroup) {
                    ViewGroup decorGroup = (ViewGroup) decor;
                    final View navbarBackgroundView = window.getNavigationBarBackgroundView();
                    zOrderChanged = navbarBackgroundView != null
                            && decorGroup.indexOfChild(navbarBackgroundView)
                            > decorGroup.indexOfChild(mNavigationBarFrame);
                }
                final boolean insetChanged = !Objects.equals(systemInsets, mLastInsets);
                if (zOrderChanged || insetChanged) {
                    final NavigationBarFrame that = mNavigationBarFrame;
                    that.post(() -> {
                        if (!that.isAttachedToWindow()) {
                            return;
                        }
                        final Insets currentSystemInsets = getSystemInsets();
                        if (!Objects.equals(currentSystemInsets, mLastInsets)) {
                            that.setLayoutParams(
                                    new FrameLayout.LayoutParams(
                                            ViewGroup.LayoutParams.MATCH_PARENT,
                                            currentSystemInsets.bottom, Gravity.BOTTOM));
                            mLastInsets = currentSystemInsets;
                        }
                        if (decor instanceof ViewGroup) {
                            ViewGroup decorGroup = (ViewGroup) decor;
                            final View navbarBackgroundView =
                                    window.getNavigationBarBackgroundView();
                            if (navbarBackgroundView != null
                                    && decorGroup.indexOfChild(navbarBackgroundView)
                                    > decorGroup.indexOfChild(that)) {
                                decorGroup.bringChildToFront(that);
                            }
                        }
                    });
                }
            }
        }

        private boolean isGesturalNavigationEnabled() {
            final Resources resources = mService.getResources();
            if (resources == null) {
                return false;
            }
            return resources.getInteger(com.android.internal.R.integer.config_navBarInteractionMode)
                    == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
        }

        @Override
        public void onViewInitialized() {
            if (mDestroyed) {
                return;
            }
            mRenderGesturalNavButtons = isGesturalNavigationEnabled();
            installNavigationBarFrameIfNecessary();
        }

        @Override
        public void onDestroy() {
            mDestroyed = true;
        }

        @Override
        public void onWindowShown() {
            if (mDestroyed || !mRenderGesturalNavButtons || mNavigationBarFrame == null) {
                return;
            }
            final Insets systemInsets = getSystemInsets();
            if (systemInsets != null) {
                if (!Objects.equals(systemInsets, mLastInsets)) {
                    mNavigationBarFrame.setLayoutParams(new NavigationBarFrame.LayoutParams(
                            ViewGroup.LayoutParams.MATCH_PARENT,
                            systemInsets.bottom, Gravity.BOTTOM));
                    mLastInsets = systemInsets;
                }
                final Window window = mService.mWindow.getWindow();
                View rawDecorView = window.getDecorView();
                if (rawDecorView instanceof ViewGroup) {
                    final ViewGroup decor = (ViewGroup) rawDecorView;
                    final View navbarBackgroundView = window.getNavigationBarBackgroundView();
                    if (navbarBackgroundView != null
                            && decor.indexOfChild(navbarBackgroundView)
                            > decor.indexOfChild(mNavigationBarFrame)) {
                        decor.bringChildToFront(mNavigationBarFrame);
                    }
                }
                mNavigationBarFrame.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public String toDebugString() {
            return "{mRenderGesturalNavButtons=" + mRenderGesturalNavButtons + "}";
        }
    }
}
+302 −0

File added.

Preview size limit exceeded, changes collapsed.

+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.inputmethodservice.navigationbar;

import android.annotation.Nullable;
import android.graphics.drawable.Drawable;

interface ButtonInterface {

    void setImageDrawable(@Nullable Drawable drawable);

    void setDarkIntensity(float intensity);

    void setDelayTouchFeedback(boolean shouldDelay);
}
+203 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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 android.inputmethodservice.navigationbar;

import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_DECAY;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_HOLD;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_SIZE;
import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVIGATION_BAR_DEADZONE_SIZE_MAX;
import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;

import android.animation.ObjectAnimator;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;

/**
 * The "dead zone" consumes unintentional taps along the top edge of the navigation bar.
 * When users are typing quickly on an IME they may attempt to hit the space bar, overshoot, and
 * accidentally hit the home button. The DeadZone expands temporarily after each tap in the UI
 * outside the navigation bar (since this is when accidental taps are more likely), then contracts
 * back over time (since a later tap might be intended for the top of the bar).
 */
final class DeadZone {
    public static final String TAG = "DeadZone";

    public static final boolean DEBUG = false;
    public static final int HORIZONTAL = 0;  // Consume taps along the top edge.
    public static final int VERTICAL = 1;  // Consume taps along the left edge.

    private static final boolean CHATTY = true; // print to logcat when we eat a click
    private final NavigationBarView mNavigationBarView;

    private boolean mShouldFlash;
    private float mFlashFrac = 0f;

    private int mSizeMax;
    private int mSizeMin;
    // Upon activity elsewhere in the UI, the dead zone will hold steady for
    // mHold ms, then move back over the course of mDecay ms
    private int mHold, mDecay;
    private boolean mVertical;
    private long mLastPokeTime;
    private int mDisplayRotation;

    private final Runnable mDebugFlash = new Runnable() {
        @Override
        public void run() {
            ObjectAnimator.ofFloat(DeadZone.this, "flash", 1f, 0f).setDuration(150).start();
        }
    };

    public DeadZone(NavigationBarView view) {
        mNavigationBarView = view;
        onConfigurationChanged(Surface.ROTATION_0);
    }

    static float lerp(float a, float b, float f) {
        return (b - a) * f + a;
    }

    private float getSize(long now) {
        if (mSizeMax == 0)
            return 0;
        long dt = (now - mLastPokeTime);
        if (dt > mHold + mDecay)
            return mSizeMin;
        if (dt < mHold)
            return mSizeMax;
        return (int) lerp(mSizeMax, mSizeMin, (float) (dt - mHold) / mDecay);
    }

    public void setFlashOnTouchCapture(boolean dbg) {
        mShouldFlash = dbg;
        mFlashFrac = 0f;
        mNavigationBarView.postInvalidate();
    }

    public void onConfigurationChanged(int rotation) {
        mDisplayRotation = rotation;

        final Resources res = mNavigationBarView.getResources();
        mHold = NAVIGATION_BAR_DEADZONE_HOLD;
        mDecay = NAVIGATION_BAR_DEADZONE_DECAY;

        mSizeMin = dpToPx(NAVIGATION_BAR_DEADZONE_SIZE, res);
        mSizeMax = dpToPx(NAVIGATION_BAR_DEADZONE_SIZE_MAX, res);
        mVertical = (res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);

        if (DEBUG) {
            Log.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
                    + (mVertical ? " vertical" : " horizontal"));
        }
        setFlashOnTouchCapture(false);   // hard-coded from "bool/config_dead_zone_flash"
    }

    // I made you a touch event...
    public boolean onTouchEvent(MotionEvent event) {
        if (DEBUG) {
            Log.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
        }

        // Don't consume events for high precision pointing devices. For this purpose a stylus is
        // considered low precision (like a finger), so its events may be consumed.
        if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
            return false;
        }

        final int action = event.getAction();
        if (action == MotionEvent.ACTION_OUTSIDE) {
            poke(event);
            return true;
        } else if (action == MotionEvent.ACTION_DOWN) {
            if (DEBUG) {
                Log.v(TAG, this + " ACTION_DOWN: " + event.getX() + "," + event.getY());
            }
            //TODO(b/205803355): call mNavBarController.touchAutoDim(mDisplayId); here
            int size = (int) getSize(event.getEventTime());
            // In the vertical orientation consume taps along the left edge.
            // In horizontal orientation consume taps along the top edge.
            final boolean consumeEvent;
            if (mVertical) {
                if (mDisplayRotation == Surface.ROTATION_270) {
                    consumeEvent = event.getX() > mNavigationBarView.getWidth() - size;
                } else {
                    consumeEvent = event.getX() < size;
                }
            } else {
                consumeEvent = event.getY() < size;
            }
            if (consumeEvent) {
                if (CHATTY) {
                    Log.v(TAG, "consuming errant click: (" + event.getX() + ","
                            + event.getY() + ")");
                }
                if (mShouldFlash) {
                    mNavigationBarView.post(mDebugFlash);
                    mNavigationBarView.postInvalidate();
                }
                return true; // ...but I eated it
            }
        }
        return false;
    }

    private void poke(MotionEvent event) {
        mLastPokeTime = event.getEventTime();
        if (DEBUG)
            Log.v(TAG, "poked! size=" + getSize(mLastPokeTime));
        if (mShouldFlash) mNavigationBarView.postInvalidate();
    }

    public void setFlash(float f) {
        mFlashFrac = f;
        mNavigationBarView.postInvalidate();
    }

    public float getFlash() {
        return mFlashFrac;
    }

    public void onDraw(Canvas can) {
        if (!mShouldFlash || mFlashFrac <= 0f) {
            return;
        }

        final int size = (int) getSize(SystemClock.uptimeMillis());
        if (mVertical) {
            if (mDisplayRotation == Surface.ROTATION_270) {
                can.clipRect(can.getWidth() - size, 0, can.getWidth(), can.getHeight());
            } else {
                can.clipRect(0, 0, size, can.getHeight());
            }
        } else {
            can.clipRect(0, 0, can.getWidth(), size);
        }

        final float frac = DEBUG ? (mFlashFrac - 0.5f) + 0.5f : mFlashFrac;
        can.drawARGB((int) (frac * 0xFF), 0xDD, 0xEE, 0xAA);

        if (DEBUG && size > mSizeMin) {
            // Very aggressive redrawing here, for debugging only
            mNavigationBarView.postInvalidateDelayed(100);
        }
    }
}
Loading