diff options
| author | Yohei Yukawa <yukawa@google.com> | 2022-01-20 21:28:34 -0800 |
|---|---|---|
| committer | Yohei Yukawa <yukawa@google.com> | 2022-01-20 21:28:34 -0800 |
| commit | ff7b453ca8e23b4ef75ba2c3f5becaf511cb07d3 (patch) | |
| tree | 3c7eb2d56e6c9489eea17aa7ffd75e89588de2eb /core/java/android/inputmethodservice/NavigationBarController.java | |
| parent | 20ba147a1138be3ea40d7e20651ac5dd83afab1e (diff) | |
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
Diffstat (limited to 'core/java/android/inputmethodservice/NavigationBarController.java')
| -rw-r--r-- | core/java/android/inputmethodservice/NavigationBarController.java | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java new file mode 100644 index 000000000000..7295b72c276b --- /dev/null +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -0,0 +1,328 @@ +/* + * 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 + "}"; + } + } +} |
