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.res.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         draggableBounds.top += margin;
158         draggableBounds.right -= getMenuWidth();
159 
160         if (includeIme && mIsImeShowing) {
161             final int imeHeight = (int) (draggableBounds.bottom - mImeTop);
162             draggableBounds.bottom -= (imeHeight + mImeShiftingSpace);
163         }
164         draggableBounds.bottom -= (calculateActualMenuHeight() + margin);
165         draggableBounds.bottom = Math.max(draggableBounds.top, draggableBounds.bottom);
166 
167         return draggableBounds;
168     }
169 
getMenuPosition()170     PointF getMenuPosition() {
171         final Rect draggableBounds = getMenuDraggableBoundsExcludeIme();
172         final float x = draggableBounds.left
173                 + draggableBounds.width() * mPercentagePosition.getPercentageX();
174 
175         float y = draggableBounds.top
176                 + draggableBounds.height() * mPercentagePosition.getPercentageY();
177 
178         // If the bottom of the menu view and overlap on the ime, its position y will be
179         // overridden with new y.
180         final float menuBottom = y + getMenuHeight() + mMargin;
181         if (mIsImeShowing && (menuBottom >= mImeTop)) {
182             y = Math.max(draggableBounds.top,
183                     mImeTop - getMenuHeight() - mMargin - mImeShiftingSpace);
184         }
185 
186         return new PointF(x, y);
187     }
188 
getContentDescription()189     String getContentDescription() {
190         return mContentDescription;
191     }
192 
getMenuBackground()193     Drawable getMenuBackground() {
194         return mBackgroundDrawable;
195     }
196 
getMenuElevation()197     int getMenuElevation() {
198         return mElevation;
199     }
200 
getMenuWidth()201     int getMenuWidth() {
202         return getMenuPadding() * 2 + getMenuIconSize();
203     }
204 
getMenuHeight()205     int getMenuHeight() {
206         return Math.min(getWindowAvailableBounds().height() - mMargin * 2,
207                 calculateActualMenuHeight());
208     }
209 
getMenuIconSize()210     int getMenuIconSize() {
211         return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize;
212     }
213 
getMenuMargin()214     private int getMenuMargin() {
215         return mMargin;
216     }
217 
getMenuPadding()218     int getMenuPadding() {
219         return mSizeType == SMALL ? mSmallPadding : mLargePadding;
220     }
221 
getMenuInsets()222     int[] getMenuInsets() {
223         final int left = isMenuOnLeftSide() ? mInset : 0;
224         final int right = isMenuOnLeftSide() ? 0 : mInset;
225 
226         return new int[]{left, 0, right, 0};
227     }
228 
getMenuMovingStateInsets()229     int[] getMenuMovingStateInsets() {
230         return new int[]{0, 0, 0, 0};
231     }
232 
getMenuMovingStateRadii()233     float[] getMenuMovingStateRadii() {
234         final float radius = getMenuRadius(mTargetFeaturesSize);
235         return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
236     }
237 
getMenuStrokeWidth()238     int getMenuStrokeWidth() {
239         return mStrokeWidth;
240     }
241 
getMenuStrokeColor()242     int getMenuStrokeColor() {
243         return mStrokeColor;
244     }
245 
getMenuRadii()246     float[] getMenuRadii() {
247         return mRadii;
248     }
249 
getMenuRadius(int itemCount)250     private int getMenuRadius(int itemCount) {
251         return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount);
252     }
253 
getMenuScrollMode()254     int getMenuScrollMode() {
255         return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER;
256     }
257 
hasExceededMaxWindowHeight()258     private boolean hasExceededMaxWindowHeight() {
259         return calculateActualMenuHeight() > getWindowAvailableBounds().height();
260     }
261 
262     @DimenRes
getSmallSize(int itemCount)263     private int getSmallSize(int itemCount) {
264         return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius;
265     }
266 
267     @DimenRes
getLargeSize(int itemCount)268     private int getLargeSize(int itemCount) {
269         return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius;
270     }
271 
createRadii(boolean isMenuOnLeftSide, float radius)272     private static float[] createRadii(boolean isMenuOnLeftSide, float radius) {
273         return isMenuOnLeftSide
274                 ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}
275                 : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
276     }
277 
getWindowAvailableBounds()278     public Rect getWindowAvailableBounds() {
279         final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
280         final WindowInsets windowInsets = windowMetrics.getWindowInsets();
281         final Insets insets = windowInsets.getInsetsIgnoringVisibility(
282                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
283 
284         final Rect bounds = new Rect(windowMetrics.getBounds());
285         bounds.left += insets.left;
286         bounds.right -= insets.right;
287         bounds.top += insets.top;
288         bounds.bottom -= insets.bottom;
289 
290         return bounds;
291     }
292 
isMenuOnLeftSide()293     boolean isMenuOnLeftSide() {
294         return mPercentagePosition.getPercentageX() < 0.5f;
295     }
296 
calculateActualMenuHeight()297     private int calculateActualMenuHeight() {
298         final int menuPadding = getMenuPadding();
299 
300         return (menuPadding + getMenuIconSize()) * mTargetFeaturesSize + menuPadding;
301     }
302 }
303