blob: 508172d13aa34f5b9b3699c1fe02d9b3f819158d [file] [log] [blame]
/*
* 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.content.Intent.ACTION_OVERLAY_CHANGED;
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.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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.os.PatternMatcher;
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.WindowManagerPolicyConstants;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
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 void setShouldShowImeSwitcherWhenImeIsShown(
boolean shouldShowImeSwitcherWhenImeIsShown) {
}
default void onSystemBarAppearanceChanged(@Appearance int appearance) {
}
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();
}
void setShouldShowImeSwitcherWhenImeIsShown(boolean shouldShowImeSwitcherWhenImeIsShown) {
mImpl.setShouldShowImeSwitcherWhenImeIsShown(shouldShowImeSwitcherWhenImeIsShown);
}
void onSystemBarAppearanceChanged(@Appearance int appearance) {
mImpl.onSystemBarAppearanceChanged(appearance);
}
String toDebugString() {
return mImpl.toDebugString();
}
private static final class Impl implements Callback {
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 mRenderGesturalNavButtons;
@Nullable
private NavigationBarFrame mNavigationBarFrame;
@Nullable
Insets mLastInsets;
@Nullable
private BroadcastReceiver mSystemOverlayChangedReceiver;
private boolean mShouldShowImeSwitcherWhenImeIsShown;
@Appearance
private int mAppearance;
@FloatRange(from = 0.0f, to = 1.0f)
private float mDarkIntensity;
@Nullable
private ValueAnimator mTintAnimator;
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;
}
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_SHOWN
: 0);
navigationBarView.setNavigationIconHints(hints);
}
} else {
mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, systemInsets.bottom, Gravity.BOTTOM));
mLastInsets = systemInsets;
}
mNavigationBarFrame.setBackground(null);
setIconTintInternal(calculateTargetDarkIntensity(mAppearance));
}
private void uninstallNavigationBarFrameIfNecessary() {
if (mNavigationBarFrame == null) {
return;
}
final ViewParent parent = mNavigationBarFrame.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(mNavigationBarFrame);
}
mNavigationBarFrame = 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) {
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);
}
});
}
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();
if (mSystemOverlayChangedReceiver == null) {
final IntentFilter intentFilter = new IntentFilter(ACTION_OVERLAY_CHANGED);
intentFilter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
intentFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL);
mSystemOverlayChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (mDestroyed) {
return;
}
mRenderGesturalNavButtons = isGesturalNavigationEnabled();
if (mRenderGesturalNavButtons) {
installNavigationBarFrameIfNecessary();
} else {
uninstallNavigationBarFrameIfNecessary();
}
}
};
mService.registerReceiver(mSystemOverlayChangedReceiver, intentFilter);
}
installNavigationBarFrameIfNecessary();
}
@Override
public void onDestroy() {
if (mDestroyed) {
return;
}
if (mTintAnimator != null) {
mTintAnimator.cancel();
mTintAnimator = null;
}
if (mSystemOverlayChangedReceiver != null) {
mService.unregisterReceiver(mSystemOverlayChangedReceiver);
mSystemOverlayChangedReceiver = null;
}
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 void setShouldShowImeSwitcherWhenImeIsShown(
boolean shouldShowImeSwitcherWhenImeIsShown) {
if (mDestroyed) {
return;
}
if (mShouldShowImeSwitcherWhenImeIsShown == shouldShowImeSwitcherWhenImeIsShown) {
return;
}
mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown;
if (mNavigationBarFrame == null) {
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_SHOWN : 0);
navigationBarView.setNavigationIconHints(hints);
}
@Override
public void onSystemBarAppearanceChanged(@Appearance int appearance) {
if (mDestroyed) {
return;
}
mAppearance = appearance;
if (mNavigationBarFrame == null) {
return;
}
final float targetDarkIntensity = calculateTargetDarkIntensity(mAppearance);
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) {
final boolean lightNavBar = (appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0;
return lightNavBar ? 1.0f : 0.0f;
}
@Override
public String toDebugString() {
return "{mRenderGesturalNavButtons=" + mRenderGesturalNavButtons
+ " mNavigationBarFrame=" + mNavigationBarFrame
+ " mShouldShowImeSwitcherWhenImeIsShown" + mShouldShowImeSwitcherWhenImeIsShown
+ " mAppearance=0x" + Integer.toHexString(mAppearance)
+ " mDarkIntensity=" + mDarkIntensity
+ "}";
}
}
}