1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility.floatingmenu;
18 
19 import static android.view.View.OVER_SCROLL_ALWAYS;
20 import static android.view.View.OVER_SCROLL_NEVER;
21 
22 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
23 
24 import android.annotation.IntDef;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.graphics.Insets;
28 import android.graphics.PointF;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Drawable;
31 import android.view.WindowInsets;
32 import android.view.WindowManager;
33 import android.view.WindowMetrics;
34 
35 import androidx.annotation.DimenRes;
36 
37 import com.android.systemui.R;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 
42 /**
43  * Provides the layout resources information of the {@link MenuView}.
44  */
45 class MenuViewAppearance {
46     private final WindowManager mWindowManager;
47     private final Resources mRes;
48     private final Position mPercentagePosition = new Position(/* percentageX= */
49             0f, /* percentageY= */ 0f);
50     private boolean mIsImeShowing;
51     // Avoid the menu view overlapping on the primary action button under the bottom as possible.
52     private int mImeShiftingSpace;
53     private int mTargetFeaturesSize;
54     private int mSizeType;
55     private int mMargin;
56     private int mSmallPadding;
57     private int mLargePadding;
58     private int mSmallIconSize;
59     private int mLargeIconSize;
60     private int mSmallSingleRadius;
61     private int mSmallMultipleRadius;
62     private int mLargeSingleRadius;
63     private int mLargeMultipleRadius;
64     private int mStrokeWidth;
65     private int mStrokeColor;
66     private int mInset;
67     private int mElevation;
68     private float mImeTop;
69     private float[] mRadii;
70     private Drawable mBackgroundDrawable;
71     private String mContentDescription;
72 
73     @IntDef({
74             SMALL,
75             MenuSizeType.LARGE
76     })
77     @Retention(RetentionPolicy.SOURCE)
78     @interface MenuSizeType {
79         int SMALL = 0;
80         int LARGE = 1;
81     }
82 
MenuViewAppearance(Context context, WindowManager windowManager)83     MenuViewAppearance(Context context, WindowManager windowManager) {
84         mWindowManager = windowManager;
85         mRes = context.getResources();
86 
87         update();
88     }
89 
update()90     void update() {
91         mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
92         mSmallPadding =
93                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
94         mLargePadding =
95                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_padding);
96         mSmallIconSize =
97                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
98         mLargeIconSize =
99                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_width_height);
100         mSmallSingleRadius =
101                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius);
102         mSmallMultipleRadius = mRes.getDimensionPixelSize(
103                 R.dimen.accessibility_floating_menu_small_multiple_radius);
104         mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
105         mLargeSingleRadius =
106                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius);
107         mLargeMultipleRadius = mRes.getDimensionPixelSize(
108                 R.dimen.accessibility_floating_menu_large_multiple_radius);
109         mStrokeWidth = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_width);
110         mStrokeColor = mRes.getColor(R.color.accessibility_floating_menu_stroke_dark);
111         mInset = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_stroke_inset);
112         mElevation = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_elevation);
113         mImeShiftingSpace = mRes.getDimensionPixelSize(
114                 R.dimen.accessibility_floating_menu_ime_shifting_space);
115         final Drawable drawable =
116                 mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
117         mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
118         mContentDescription = mRes.getString(
119                 com.android.internal.R.string.accessibility_select_shortcut_menu_title);
120     }
121 
setSizeType(int sizeType)122     void setSizeType(int sizeType) {
123         mSizeType = sizeType;
124 
125         mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
126     }
127 
setTargetFeaturesSize(int targetFeaturesSize)128     void setTargetFeaturesSize(int targetFeaturesSize) {
129         mTargetFeaturesSize = targetFeaturesSize;
130 
131         mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize));
132     }
133 
setPercentagePosition(Position percentagePosition)134     void setPercentagePosition(Position percentagePosition) {
135         mPercentagePosition.update(percentagePosition);
136 
137         mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
138     }
139 
onImeVisibilityChanged(boolean imeShowing, float imeTop)140     void onImeVisibilityChanged(boolean imeShowing, float imeTop) {
141         mIsImeShowing = imeShowing;
142         mImeTop = imeTop;
143     }
144 
getMenuDraggableBounds()145     Rect getMenuDraggableBounds() {
146         return getMenuDraggableBoundsWith(/* includeIme= */ true);
147     }
148 
getMenuDraggableBoundsExcludeIme()149     Rect getMenuDraggableBoundsExcludeIme() {
150         return getMenuDraggableBoundsWith(/* includeIme= */ false);
151     }
152 
getMenuDraggableBoundsWith(boolean includeIme)153     private Rect getMenuDraggableBoundsWith(boolean includeIme) {
154         final int margin = getMenuMargin();
155         final Rect draggableBounds = new Rect(getWindowAvailableBounds());
156 
157         // Initializes start position for mapping the translation of the menu view.
158         draggableBounds.offsetTo(/* newLeft= */ 0, /* newTop= */ 0);
159 
160         draggableBounds.top += margin;
161         draggableBounds.right -= getMenuWidth();
162 
163         if (includeIme && mIsImeShowing) {
164             final int imeHeight = (int) (draggableBounds.bottom - mImeTop);
165             draggableBounds.bottom -= (imeHeight + mImeShiftingSpace);
166         }
167         draggableBounds.bottom -= (calculateActualMenuHeight() + margin);
168         draggableBounds.bottom = Math.max(draggableBounds.top, draggableBounds.bottom);
169 
170         return draggableBounds;
171     }
172 
getMenuPosition()173     PointF getMenuPosition() {
174         final Rect draggableBounds = getMenuDraggableBoundsExcludeIme();
175         final float x = draggableBounds.left
176                 + draggableBounds.width() * mPercentagePosition.getPercentageX();
177 
178         float y = draggableBounds.top
179                 + draggableBounds.height() * mPercentagePosition.getPercentageY();
180 
181         // If the bottom of the menu view and overlap on the ime, its position y will be
182         // overridden with new y.
183         final float menuBottom = y + getMenuHeight() + mMargin;
184         if (mIsImeShowing && (menuBottom >= mImeTop)) {
185             y = Math.max(draggableBounds.top,
186                     mImeTop - getMenuHeight() - mMargin - mImeShiftingSpace);
187         }
188 
189         return new PointF(x, y);
190     }
191 
getContentDescription()192     String getContentDescription() {
193         return mContentDescription;
194     }
195 
getMenuBackground()196     Drawable getMenuBackground() {
197         return mBackgroundDrawable;
198     }
199 
getMenuElevation()200     int getMenuElevation() {
201         return mElevation;
202     }
203 
getMenuWidth()204     int getMenuWidth() {
205         return getMenuPadding() * 2 + getMenuIconSize();
206     }
207 
getMenuHeight()208     int getMenuHeight() {
209         return Math.min(getWindowAvailableBounds().height() - mMargin * 2,
210                 calculateActualMenuHeight());
211     }
212 
getMenuIconSize()213     int getMenuIconSize() {
214         return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize;
215     }
216 
getMenuMargin()217     private int getMenuMargin() {
218         return mMargin;
219     }
220 
getMenuPadding()221     int getMenuPadding() {
222         return mSizeType == SMALL ? mSmallPadding : mLargePadding;
223     }
224 
getMenuInsets()225     int[] getMenuInsets() {
226         final int left = isMenuOnLeftSide() ? mInset : 0;
227         final int right = isMenuOnLeftSide() ? 0 : mInset;
228 
229         return new int[]{left, 0, right, 0};
230     }
231 
getMenuMovingStateInsets()232     int[] getMenuMovingStateInsets() {
233         return new int[]{0, 0, 0, 0};
234     }
235 
getMenuMovingStateRadii()236     float[] getMenuMovingStateRadii() {
237         final float radius = getMenuRadius(mTargetFeaturesSize);
238         return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
239     }
240 
getMenuStrokeWidth()241     int getMenuStrokeWidth() {
242         return mStrokeWidth;
243     }
244 
getMenuStrokeColor()245     int getMenuStrokeColor() {
246         return mStrokeColor;
247     }
248 
getMenuRadii()249     float[] getMenuRadii() {
250         return mRadii;
251     }
252 
getMenuRadius(int itemCount)253     private int getMenuRadius(int itemCount) {
254         return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount);
255     }
256 
getMenuScrollMode()257     int getMenuScrollMode() {
258         return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER;
259     }
260 
hasExceededMaxWindowHeight()261     private boolean hasExceededMaxWindowHeight() {
262         return calculateActualMenuHeight() > getWindowAvailableBounds().height();
263     }
264 
265     @DimenRes
getSmallSize(int itemCount)266     private int getSmallSize(int itemCount) {
267         return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius;
268     }
269 
270     @DimenRes
getLargeSize(int itemCount)271     private int getLargeSize(int itemCount) {
272         return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius;
273     }
274 
createRadii(boolean isMenuOnLeftSide, float radius)275     private static float[] createRadii(boolean isMenuOnLeftSide, float radius) {
276         return isMenuOnLeftSide
277                 ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}
278                 : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
279     }
280 
getWindowAvailableBounds()281     private Rect getWindowAvailableBounds() {
282         final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
283         final WindowInsets windowInsets = windowMetrics.getWindowInsets();
284         final Insets insets = windowInsets.getInsetsIgnoringVisibility(
285                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
286 
287         final Rect bounds = new Rect(windowMetrics.getBounds());
288         bounds.left += insets.left;
289         bounds.right -= insets.right;
290         bounds.top += insets.top;
291         bounds.bottom -= insets.bottom;
292 
293         return bounds;
294     }
295 
isMenuOnLeftSide()296     boolean isMenuOnLeftSide() {
297         return mPercentagePosition.getPercentageX() < 0.5f;
298     }
299 
calculateActualMenuHeight()300     private int calculateActualMenuHeight() {
301         final int menuPadding = getMenuPadding();
302 
303         return (menuPadding + getMenuIconSize()) * mTargetFeaturesSize + menuPadding;
304     }
305 }
306