/* * 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 static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import android.animation.ValueAnimator; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.StatusBarManager; import android.graphics.Color; 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.ViewParent; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController.Appearance; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import com.android.internal.inputmethod.InputMethodNavButtonFlags; import java.util.Objects; /** * This class hides details behind {@link InputMethodService#canImeRenderGesturalNavButtons()} from * {@link InputMethodService}. * *
All the package-private methods are no-op when * {@link InputMethodService#canImeRenderGesturalNavButtons()} returns {@code false}.
*/ final class NavigationBarController { private interface Callback { default void updateInsets(@NonNull InputMethodService.Insets originalInsets) { } default void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest) { } default void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { } default void onViewInitialized() { } default void onWindowShown() { } default void onDestroy() { } default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { } default boolean isShown() { return false; } 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; } /** * Update the given insets to be at least as big as the IME navigation bar, when visible. * * @param originalInsets the insets to check and modify to include the IME navigation bar. */ void updateInsets(@NonNull InputMethodService.Insets originalInsets) { mImpl.updateInsets(originalInsets); } void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest) { mImpl.updateTouchableInsets(originalInsets, dest); } void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { mImpl.onSoftInputWindowCreated(softInputWindow); } void onViewInitialized() { mImpl.onViewInitialized(); } void onWindowShown() { mImpl.onWindowShown(); } void onDestroy() { mImpl.onDestroy(); } void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { mImpl.onNavButtonFlagsChanged(navButtonFlags); } /** * Returns whether the IME navigation bar is currently shown. */ boolean isShown() { return mImpl.isShown(); } String toDebugString() { return mImpl.toDebugString(); } private static final class Impl implements Callback, Window.DecorCallback { private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700; // Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE private static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f); @NonNull private final InputMethodService mService; private boolean mDestroyed = false; private boolean mImeDrawsImeNavBar; @Nullable private NavigationBarFrame mNavigationBarFrame; @Nullable Insets mLastInsets; private boolean mShouldShowImeSwitcherWhenImeIsShown; @Appearance private int mAppearance; @FloatRange(from = 0.0f, to = 1.0f) private float mDarkIntensity; @Nullable private ValueAnimator mTintAnimator; private boolean mDrawLegacyNavigationBarBackground; private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; 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 (!mImeDrawsImeNavBar) { return; } if (mNavigationBarFrame != null) { 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 | (mShouldShowImeSwitcherWhenImeIsShown ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); navigationBarView.setNavigationIconHints(hints); } } else { mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, systemInsets.bottom, Gravity.BOTTOM)); mLastInsets = systemInsets; } if (mDrawLegacyNavigationBarBackground) { mNavigationBarFrame.setBackgroundColor(Color.BLACK); } else { mNavigationBarFrame.setBackground(null); } setIconTintInternal(calculateTargetDarkIntensity(mAppearance, mDrawLegacyNavigationBarBackground)); if (ENABLE_HIDE_IME_CAPTION_BAR) { mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> { if (mNavigationBarFrame != null) { boolean visible = insets.isVisible(captionBar()); mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); } return view.onApplyWindowInsets(insets); }); } } private void uninstallNavigationBarFrameIfNecessary() { if (mNavigationBarFrame == null) { return; } final ViewParent parent = mNavigationBarFrame.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(mNavigationBarFrame); } if (ENABLE_HIDE_IME_CAPTION_BAR) { mNavigationBarFrame.setOnApplyWindowInsetsListener(null); } mNavigationBarFrame = null; } @Override public void updateInsets(@NonNull InputMethodService.Insets originalInsets) { if (!mImeDrawsImeNavBar || mNavigationBarFrame == null || mNavigationBarFrame.getVisibility() != View.VISIBLE || mService.isFullscreenMode()) { return; } final int[] loc = new int[2]; mNavigationBarFrame.getLocationInWindow(loc); if (originalInsets.contentTopInsets > loc[1]) { originalInsets.contentTopInsets = loc[1]; } if (originalInsets.visibleTopInsets > loc[1]) { originalInsets.visibleTopInsets = loc[1]; } } @Override public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest) { if (!mImeDrawsImeNavBar || mNavigationBarFrame == null) { return; } final Insets systemInsets = getSystemInsets(); if (systemInsets != null) { final Window window = mService.mWindow.getWindow(); final View decor = window.getDecorView(); // If the extract view is shown, everything is touchable, so no need to update // touchable insets, but we still update normal insets below. if (!mService.isExtractViewShown()) { Region touchableRegion = null; final View inputFrame = mService.mInputFrame; switch (originalInsets.touchableInsets) { case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME: if (inputFrame.getVisibility() == View.VISIBLE) { inputFrame.getLocationInWindow(mTempPos); mTempRect.set(mTempPos[0], mTempPos[1], mTempPos[0] + inputFrame.getWidth(), mTempPos[1] + inputFrame.getHeight()); touchableRegion = new Region(mTempRect); } break; case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT: if (inputFrame.getVisibility() == View.VISIBLE) { inputFrame.getLocationInWindow(mTempPos); mTempRect.set(mTempPos[0], originalInsets.contentTopInsets, mTempPos[0] + inputFrame.getWidth(), mTempPos[1] + inputFrame.getHeight()); touchableRegion = new Region(mTempRect); } break; case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE: if (inputFrame.getVisibility() == View.VISIBLE) { inputFrame.getLocationInWindow(mTempPos); mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets, mTempPos[0] + inputFrame.getWidth(), mTempPos[1] + inputFrame.getHeight()); touchableRegion = new Region(mTempRect); } break; case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION: touchableRegion = new Region(); touchableRegion.set(originalInsets.touchableRegion); break; } // Hereafter "mTempRect" means a navigation bar rect. mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom, decor.getRight(), decor.getBottom()); if (touchableRegion == null) { touchableRegion = new Region(mTempRect); } else { touchableRegion.union(mTempRect); } dest.touchableRegion.set(touchableRegion); dest.setTouchableInsets( ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); } // TODO(b/215443343): See if we can use View#OnLayoutChangeListener(). // TODO(b/215443343): 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) { scheduleRelayout(); } } } private void scheduleRelayout() { // Capture the current frame object in case the object is replaced or cleared later. final NavigationBarFrame frame = mNavigationBarFrame; frame.post(() -> { if (mDestroyed) { return; } if (!frame.isAttachedToWindow()) { return; } final Window window = mService.mWindow.getWindow(); if (window == null) { return; } final View decor = window.peekDecorView(); if (decor == null) { return; } if (!(decor instanceof ViewGroup)) { return; } final ViewGroup decorGroup = (ViewGroup) decor; final Insets currentSystemInsets = getSystemInsets(); if (!Objects.equals(currentSystemInsets, mLastInsets)) { frame.setLayoutParams(new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, currentSystemInsets.bottom, Gravity.BOTTOM)); mLastInsets = currentSystemInsets; } final View navbarBackgroundView = window.getNavigationBarBackgroundView(); if (navbarBackgroundView != null && decorGroup.indexOfChild(navbarBackgroundView) > decorGroup.indexOfChild(frame)) { decorGroup.bringChildToFront(frame); } }); } @Override public void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { final Window window = softInputWindow.getWindow(); mAppearance = window.getSystemBarAppearance(); window.setDecorCallback(this); } @Override public void onViewInitialized() { if (mDestroyed) { return; } installNavigationBarFrameIfNecessary(); } @Override public void onDestroy() { if (mDestroyed) { return; } if (mTintAnimator != null) { mTintAnimator.cancel(); mTintAnimator = null; } mDestroyed = true; } @Override public void onWindowShown() { if (mDestroyed || !mImeDrawsImeNavBar || 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); } } if (!ENABLE_HIDE_IME_CAPTION_BAR) { mNavigationBarFrame.setVisibility(View.VISIBLE); } } } @Override public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { if (mDestroyed) { return; } final boolean imeDrawsImeNavBar = (navButtonFlags & InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR) != 0; final boolean shouldShowImeSwitcherWhenImeIsShown = (navButtonFlags & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) != 0; mImeDrawsImeNavBar = imeDrawsImeNavBar; final boolean prevShouldShowImeSwitcherWhenImeIsShown = mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; if (ENABLE_HIDE_IME_CAPTION_BAR) { mService.mWindow.getWindow().getDecorView().getWindowInsetsController() .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight()); } if (imeDrawsImeNavBar) { installNavigationBarFrameIfNecessary(); if (mNavigationBarFrame == null) { return; } if (mShouldShowImeSwitcherWhenImeIsShown == prevShouldShowImeSwitcherWhenImeIsShown) { return; } final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( NavigationBarView.class::isInstance); if (navigationBarView == null) { return; } final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT | (shouldShowImeSwitcherWhenImeIsShown ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); navigationBarView.setNavigationIconHints(hints); } else { uninstallNavigationBarFrameIfNecessary(); } } @Override public void onSystemBarAppearanceChanged(@Appearance int appearance) { if (mDestroyed) { return; } mAppearance = appearance; if (mNavigationBarFrame == null) { return; } final float targetDarkIntensity = calculateTargetDarkIntensity(mAppearance, mDrawLegacyNavigationBarBackground); if (mTintAnimator != null) { mTintAnimator.cancel(); } mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity); mTintAnimator.addUpdateListener( animation -> setIconTintInternal((Float) animation.getAnimatedValue())); mTintAnimator.setDuration(DEFAULT_COLOR_ADAPT_TRANSITION_TIME); mTintAnimator.setStartDelay(0); mTintAnimator.setInterpolator(LEGACY_DECELERATE); mTintAnimator.start(); } private void setIconTintInternal(float darkIntensity) { mDarkIntensity = darkIntensity; if (mNavigationBarFrame == null) { return; } final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate(NavigationBarView.class::isInstance); if (navigationBarView == null) { return; } navigationBarView.setDarkIntensity(darkIntensity); } @FloatRange(from = 0.0f, to = 1.0f) private static float calculateTargetDarkIntensity(@Appearance int appearance, boolean drawLegacyNavigationBarBackground) { final boolean lightNavBar = !drawLegacyNavigationBarBackground && (appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0; return lightNavBar ? 1.0f : 0.0f; } @Override public boolean onDrawLegacyNavigationBarBackgroundChanged( boolean drawLegacyNavigationBarBackground) { if (mDestroyed) { return false; } if (drawLegacyNavigationBarBackground != mDrawLegacyNavigationBarBackground) { mDrawLegacyNavigationBarBackground = drawLegacyNavigationBarBackground; if (mNavigationBarFrame != null) { if (mDrawLegacyNavigationBarBackground) { mNavigationBarFrame.setBackgroundColor(Color.BLACK); } else { mNavigationBarFrame.setBackground(null); } scheduleRelayout(); } onSystemBarAppearanceChanged(mAppearance); } return drawLegacyNavigationBarBackground; } /** * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead. */ private int getImeCaptionBarHeight() { return mImeDrawsImeNavBar ? mService.getResources().getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_frame_height) : 0; } @Override public boolean isShown() { return mNavigationBarFrame != null && mNavigationBarFrame.getVisibility() == View.VISIBLE; } @Override public String toDebugString() { return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar + " mNavigationBarFrame=" + mNavigationBarFrame + " mShouldShowImeSwitcherWhenImeIsShown=" + mShouldShowImeSwitcherWhenImeIsShown + " mAppearance=0x" + Integer.toHexString(mAppearance) + " mDarkIntensity=" + mDarkIntensity + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground + "}"; } } }