diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6577b07f71390..6dcd675862bcb 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1341,6 +1341,7 @@ 56dp 35dp 35dp + 48dp 15dp 8dp diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index 396f584d76a6b..0cfd6a4689f4a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -345,7 +345,7 @@ class MenuAnimationController { } private void constrainPositionAndUpdate(PointF position) { - final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); + final Rect draggableBounds = mMenuView.getMenuDraggableBoundsExcludeIme(); // Have the space gap margin between the top bound and the menu view, so actually the // position y range needs to cut the margin. position.offset(-draggableBounds.left, -draggableBounds.top); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 6a14af52fbaff..2ecb58c84ff63 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -219,6 +219,14 @@ class MenuView extends FrameLayout implements return mMenuViewAppearance.getMenuDraggableBounds(); } + Rect getMenuDraggableBoundsExcludeIme() { + return mMenuViewAppearance.getMenuDraggableBoundsExcludeIme(); + } + + int getMenuHeight() { + return mMenuViewAppearance.getMenuHeight(); + } + void persistPositionAndUpdateEdge(Position percentagePosition) { mMenuViewModel.updateMenuSavingPosition(percentagePosition); mMenuViewAppearance.setPercentagePosition(percentagePosition); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java index 4a9807febc7f7..a7cdeab7c1279 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java @@ -47,6 +47,9 @@ class MenuViewAppearance { private final Resources mRes; private final Position mPercentagePosition = new Position(/* percentageX= */ 0f, /* percentageY= */ 0f); + private boolean mIsImeShowing; + // Avoid the menu view overlapping on the primary action button under the bottom as possible. + private int mImeShiftingSpace; private int mTargetFeaturesSize; private int mSizeType; private int mMargin; @@ -62,6 +65,7 @@ class MenuViewAppearance { private int mStrokeColor; private int mInset; private int mElevation; + private float mImeTop; private float[] mRadii; private Drawable mBackgroundDrawable; private String mContentDescription; @@ -106,6 +110,8 @@ class MenuViewAppearance { mStrokeColor = mRes.getColor(R.color.accessibility_floating_menu_stroke_dark); mInset = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset); mElevation = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation); + mImeShiftingSpace = mRes.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_ime_shifting_space); final Drawable drawable = mRes.getDrawable(R.drawable.accessibility_floating_menu_background); mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable}); @@ -131,29 +137,56 @@ class MenuViewAppearance { mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize)); } + void onImeVisibilityChanged(boolean imeShowing, float imeTop) { + mIsImeShowing = imeShowing; + mImeTop = imeTop; + } + Rect getMenuDraggableBounds() { + return getMenuDraggableBoundsWith(/* includeIme= */ true); + } + + Rect getMenuDraggableBoundsExcludeIme() { + return getMenuDraggableBoundsWith(/* includeIme= */ false); + } + + private Rect getMenuDraggableBoundsWith(boolean includeIme) { final int margin = getMenuMargin(); - final Rect draggableBounds = getWindowAvailableBounds(); + final Rect draggableBounds = new Rect(getWindowAvailableBounds()); // Initializes start position for mapping the translation of the menu view. draggableBounds.offsetTo(/* newLeft= */ 0, /* newTop= */ 0); draggableBounds.top += margin; draggableBounds.right -= getMenuWidth(); - draggableBounds.bottom -= Math.min( - getWindowAvailableBounds().height() - draggableBounds.top, - calculateActualMenuHeight() + margin); + + if (includeIme && mIsImeShowing) { + final int imeHeight = (int) (draggableBounds.bottom - mImeTop); + draggableBounds.bottom -= (imeHeight + mImeShiftingSpace); + } + draggableBounds.bottom -= (calculateActualMenuHeight() + margin); + draggableBounds.bottom = Math.max(draggableBounds.top, draggableBounds.bottom); + return draggableBounds; } PointF getMenuPosition() { - final Rect draggableBounds = getMenuDraggableBounds(); + final Rect draggableBounds = getMenuDraggableBoundsExcludeIme(); + final float x = draggableBounds.left + + draggableBounds.width() * mPercentagePosition.getPercentageX(); - return new PointF( - draggableBounds.left - + draggableBounds.width() * mPercentagePosition.getPercentageX(), - draggableBounds.top - + draggableBounds.height() * mPercentagePosition.getPercentageY()); + float y = draggableBounds.top + + draggableBounds.height() * mPercentagePosition.getPercentageY(); + + // If the bottom of the menu view and overlap on the ime, its position y will be + // overridden with new y. + final float menuBottom = y + getMenuHeight() + mMargin; + if (mIsImeShowing && (menuBottom >= mImeTop)) { + y = Math.max(draggableBounds.top, + mImeTop - getMenuHeight() - mMargin - mImeShiftingSpace); + } + + return new PointF(x, y); } String getContentDescription() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index b8f14aef648a1..c42943cf56e28 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -16,6 +16,10 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.view.WindowInsets.Type.ime; + +import static androidx.core.view.WindowInsetsCompat.Type; + import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE; import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; @@ -26,13 +30,16 @@ import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.util.PluralsMessageFormatter; import android.view.MotionEvent; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.TextView; @@ -62,14 +69,17 @@ import java.util.Map; class MenuViewLayer extends FrameLayout { private static final int SHOW_MESSAGE_DELAY_MS = 3000; + private final WindowManager mWindowManager; private final MenuView mMenuView; private final MenuMessageView mMessageView; private final DismissView mDismissView; + private final MenuViewAppearance mMenuViewAppearance; private final MenuAnimationController mMenuAnimationController; private final AccessibilityManager mAccessibilityManager; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final IAccessibilityFloatingMenu mFloatingMenu; private final DismissAnimationController mDismissAnimationController; + private final Rect mImeInsetsRect = new Rect(); @IntDef({ LayerIndex.MENU_VIEW, @@ -111,13 +121,13 @@ class MenuViewLayer extends FrameLayout { AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) { super(context); + mWindowManager = windowManager; mAccessibilityManager = accessibilityManager; mFloatingMenu = floatingMenu; final MenuViewModel menuViewModel = new MenuViewModel(context); - final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, - windowManager); - mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); + mMenuViewAppearance = new MenuViewAppearance(context, windowManager); + mMenuView = new MenuView(context, menuViewModel, mMenuViewAppearance); mMenuAnimationController = mMenuView.getMenuAnimationController(); mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); @@ -200,6 +210,7 @@ class MenuViewLayer extends FrameLayout { super.onAttachedToWindow(); mMenuView.show(); + setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets)); mMessageView.setUndoListener(view -> undo()); mContext.registerComponentCallbacks(mDismissAnimationController); } @@ -209,10 +220,35 @@ class MenuViewLayer extends FrameLayout { super.onDetachedFromWindow(); mMenuView.hide(); + setOnApplyWindowInsetsListener(null); mHandler.removeCallbacksAndMessages(/* token= */ null); mContext.unregisterComponentCallbacks(mDismissAnimationController); } + private WindowInsets onWindowInsetsApplied(WindowInsets insets) { + final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + final WindowInsets windowInsets = windowMetrics.getWindowInsets(); + final Rect imeInsetsRect = windowInsets.getInsets(ime()).toRect(); + if (!imeInsetsRect.equals(mImeInsetsRect)) { + final Rect windowBounds = new Rect(windowMetrics.getBounds()); + final Rect systemBarsAndDisplayCutoutInsetsRect = + windowInsets.getInsetsIgnoringVisibility( + Type.systemBars() | Type.displayCutout()).toRect(); + final float imeTop = + windowBounds.height() - systemBarsAndDisplayCutoutInsetsRect.top + - imeInsetsRect.bottom; + + mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop); + + mMenuView.onEdgeChanged(); + mMenuView.onPositionChanged(); + + mImeInsetsRect.set(imeInsetsRect); + } + + return insets; + } + private void hideMenuAndShowMessage() { final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( SHOW_MESSAGE_DELAY_MS, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 2d5188fcb95ac..3e0aad8a17f8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -18,12 +18,17 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static android.view.WindowInsets.Type.displayCutout; +import static android.view.WindowInsets.Type.ime; +import static android.view.WindowInsets.Type.systemBars; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,13 +37,18 @@ import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -68,9 +78,20 @@ public class MenuViewLayerTest extends SysuiTestCase { private static final ComponentName TEST_SELECT_TO_SPEAK_COMPONENT_NAME = new ComponentName( SELECT_TO_SPEAK_PACKAGE_NAME, SELECT_TO_SPEAK_SERVICE_NAME); + private static final int DISPLAY_WINDOW_WIDTH = 1080; + private static final int DISPLAY_WINDOW_HEIGHT = 2340; + private static final int STATUS_BAR_HEIGHT = 75; + private static final int NAVIGATION_BAR_HEIGHT = 125; + private static final int IME_HEIGHT = 350; + private static final int IME_TOP = + DISPLAY_WINDOW_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT - IME_HEIGHT; + private MenuViewLayer mMenuViewLayer; private String mLastAccessibilityButtonTargets; private String mLastEnabledAccessibilityServices; + private WindowMetrics mWindowMetrics; + private MenuView mMenuView; + private MenuAnimationController mMenuAnimationController; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -78,14 +99,24 @@ public class MenuViewLayerTest extends SysuiTestCase { @Mock private IAccessibilityFloatingMenu mFloatingMenu; + @Mock + private WindowManager mStubWindowManager; + @Mock private AccessibilityManager mStubAccessibilityManager; @Before public void setUp() throws Exception { - final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, mStubAccessibilityManager, + final Rect mDisplayBounds = new Rect(); + mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH, + DISPLAY_WINDOW_HEIGHT); + mWindowMetrics = spy(new WindowMetrics(mDisplayBounds, fakeDisplayInsets())); + doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics(); + + mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager, mFloatingMenu); + mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); + mMenuAnimationController = mMenuView.getMenuAnimationController(); mLastAccessibilityButtonTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(), @@ -93,6 +124,12 @@ public class MenuViewLayerTest extends SysuiTestCase { mLastEnabledAccessibilityServices = Settings.Secure.getStringForUser(mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT); + + mMenuViewLayer.onAttachedToWindow(); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT); } @After @@ -103,6 +140,8 @@ public class MenuViewLayerTest extends SysuiTestCase { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices, UserHandle.USER_CURRENT); + + mMenuViewLayer.onDetachedFromWindow(); } @Test @@ -168,4 +207,69 @@ public class MenuViewLayerTest extends SysuiTestCase { assertThat(value).isEqualTo(""); } + + @Test + public void showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition() { + final float menuTop = STATUS_BAR_HEIGHT + 100; + mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop)); + + dispatchShowingImeInsets(); + + assertThat(mMenuView.getTranslationX()).isEqualTo(0); + assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop); + } + + @Test + public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() { + final float menuTop = IME_TOP + 100; + mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop)); + + dispatchShowingImeInsets(); + + final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight(); + assertThat(mMenuView.getTranslationX()).isEqualTo(0); + assertThat(menuBottom).isLessThan(IME_TOP); + } + + @Test + public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() { + final float menuTop = IME_TOP + 200; + mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop)); + dispatchShowingImeInsets(); + + dispatchHidingImeInsets(); + + assertThat(mMenuView.getTranslationX()).isEqualTo(0); + assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop); + } + + private void dispatchShowingImeInsets() { + final WindowInsets fakeShowingImeInsets = fakeImeInsets(/* isImeVisible= */ true); + doReturn(fakeShowingImeInsets).when(mWindowMetrics).getWindowInsets(); + mMenuViewLayer.dispatchApplyWindowInsets(fakeShowingImeInsets); + } + + private void dispatchHidingImeInsets() { + final WindowInsets fakeHidingImeInsets = fakeImeInsets(/* isImeVisible= */ false); + doReturn(fakeHidingImeInsets).when(mWindowMetrics).getWindowInsets(); + mMenuViewLayer.dispatchApplyWindowInsets(fakeHidingImeInsets); + } + + private WindowInsets fakeDisplayInsets() { + return new WindowInsets.Builder() + .setVisible(systemBars() | displayCutout(), /* visible= */ true) + .setInsets(systemBars() | displayCutout(), + Insets.of(/* left= */ 0, STATUS_BAR_HEIGHT, /* right= */ 0, + NAVIGATION_BAR_HEIGHT)) + .build(); + } + + private WindowInsets fakeImeInsets(boolean isImeVisible) { + final int bottom = isImeVisible ? (IME_HEIGHT + NAVIGATION_BAR_HEIGHT) : 0; + return new WindowInsets.Builder() + .setVisible(ime(), isImeVisible) + .setInsets(ime(), + Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, bottom)) + .build(); + } }