1 /*
2  * Copyright (C) 2023 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.accessibilitymenu.view;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE;
21 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
22 
23 import static java.lang.Math.max;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.content.Context;
28 import android.content.res.Configuration;
29 import android.graphics.Insets;
30 import android.graphics.PixelFormat;
31 import android.graphics.Rect;
32 import android.hardware.display.DisplayManager;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.view.Display;
36 import android.view.Gravity;
37 import android.view.LayoutInflater;
38 import android.view.Surface;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.WindowInsets;
42 import android.view.WindowManager;
43 import android.view.WindowMetrics;
44 import android.view.accessibility.AccessibilityManager;
45 import android.widget.FrameLayout;
46 import android.widget.TextView;
47 
48 import androidx.annotation.NonNull;
49 
50 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
51 import com.android.systemui.accessibility.accessibilitymenu.R;
52 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
53 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to
60  * configure or update Accessibility menu layout when orientation and display size changed, and
61  * functions to toggle menu visibility when button clicked or screen off.
62  */
63 public class A11yMenuOverlayLayout {
64 
65     /** Predefined default shortcuts when large button setting is off. */
66     private static final int[] SHORTCUT_LIST_DEFAULT = {
67         A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
68         A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
69         A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
70         A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
71         A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
72         A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
73         A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
74         A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
75         A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
76         A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
77         A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
78         A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
79     };
80 
81     /** Predefined default shortcuts when large button setting is on. */
82     private static final int[] LARGE_SHORTCUT_LIST_DEFAULT = {
83             A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
84             A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
85             A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
86             A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
87             A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
88             A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
89             A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
90             A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
91             A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
92             A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
93             A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
94             A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
95     };
96 
97 
98 
99     private final AccessibilityMenuService mService;
100     private final WindowManager mWindowManager;
101     private final DisplayManager mDisplayManager;
102     private ViewGroup mLayout;
103     private WindowManager.LayoutParams mLayoutParameter;
104     private A11yMenuViewPager mA11yMenuViewPager;
105     private Handler mHandler;
106     private AccessibilityManager mAccessibilityManager;
107 
A11yMenuOverlayLayout(AccessibilityMenuService service)108     public A11yMenuOverlayLayout(AccessibilityMenuService service) {
109         mService = service;
110         mWindowManager = mService.getSystemService(WindowManager.class);
111         mDisplayManager = mService.getSystemService(DisplayManager.class);
112         configureLayout();
113         mHandler = new Handler(Looper.getMainLooper());
114         mAccessibilityManager = mService.getSystemService(AccessibilityManager.class);
115     }
116 
117     /** Creates Accessibility menu layout and configure layout parameters. */
configureLayout()118     public View configureLayout() {
119         return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX);
120     }
121 
122     // TODO(b/78292783): Find a better way to inflate layout in the test.
123     /**
124      * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager.
125      *
126      * @param pageIndex the index of the ViewPager to show.
127      */
configureLayout(int pageIndex)128     public View configureLayout(int pageIndex) {
129 
130         int lastVisibilityState = View.GONE;
131         if (mLayout != null) {
132             lastVisibilityState = mLayout.getVisibility();
133             mWindowManager.removeView(mLayout);
134             mLayout = null;
135         }
136 
137         if (mLayoutParameter == null) {
138             initLayoutParams();
139         }
140 
141         final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
142         final Context context = mService.createDisplayContext(display).createWindowContext(
143                 TYPE_ACCESSIBILITY_OVERLAY, null);
144         mLayout = new FrameLayout(context);
145         updateLayoutPosition();
146         inflateLayoutAndSetOnTouchListener(mLayout, context);
147         mA11yMenuViewPager = new A11yMenuViewPager(mService, context);
148         mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex);
149         mWindowManager.addView(mLayout, mLayoutParameter);
150         mLayout.setVisibility(lastVisibilityState);
151 
152         return mLayout;
153     }
154 
clearLayout()155     public void clearLayout() {
156         if (mLayout != null) {
157             mWindowManager.removeView(mLayout);
158             mLayout.setOnTouchListener(null);
159             mLayout = null;
160         }
161     }
162 
163     /** Updates view layout with new layout parameters only. */
updateViewLayout()164     public void updateViewLayout() {
165         if (mLayout == null || mLayoutParameter == null) {
166             return;
167         }
168         updateLayoutPosition();
169         mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
170     }
171 
initLayoutParams()172     private void initLayoutParams() {
173         mLayoutParameter = new WindowManager.LayoutParams();
174         mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
175         mLayoutParameter.format = PixelFormat.TRANSLUCENT;
176         mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
177         mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
178         mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name));
179     }
180 
inflateLayoutAndSetOnTouchListener(ViewGroup view, Context displayContext)181     private void inflateLayoutAndSetOnTouchListener(ViewGroup view, Context displayContext) {
182         LayoutInflater inflater = LayoutInflater.from(displayContext);
183         inflater.inflate(R.layout.paged_menu, view);
184         view.setOnTouchListener(mService);
185     }
186 
187     /**
188      * Loads shortcut data from default shortcut ID array.
189      *
190      * @return A list of default shortcuts
191      */
createShortcutList()192     private List<A11yMenuShortcut> createShortcutList() {
193         List<A11yMenuShortcut> shortcutList = new ArrayList<>();
194 
195         for (int shortcutId :
196                 (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)
197                         ? LARGE_SHORTCUT_LIST_DEFAULT : SHORTCUT_LIST_DEFAULT)) {
198             shortcutList.add(new A11yMenuShortcut(shortcutId));
199         }
200         return shortcutList;
201     }
202 
203     /** Updates a11y menu layout position by configuring layout params. */
updateLayoutPosition()204     private void updateLayoutPosition() {
205         final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
206         final Configuration configuration = mService.getResources().getConfiguration();
207         final int orientation = configuration.orientation;
208         if (display != null && orientation == Configuration.ORIENTATION_LANDSCAPE) {
209             final boolean ltr = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
210             switch (display.getRotation()) {
211                 case Surface.ROTATION_0:
212                 case Surface.ROTATION_180:
213                     mLayoutParameter.gravity =
214                             (ltr ? Gravity.END : Gravity.START) | Gravity.BOTTOM
215                                     | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
216                     mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
217                     mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
218                     mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
219                     mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
220                     mLayout.setBackgroundResource(R.drawable.shadow_90deg);
221                     break;
222                 case Surface.ROTATION_90:
223                 case Surface.ROTATION_270:
224                     mLayoutParameter.gravity =
225                             (ltr ? Gravity.START : Gravity.END) | Gravity.BOTTOM
226                                     | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
227                     mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
228                     mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
229                     mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
230                     mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
231                     mLayout.setBackgroundResource(R.drawable.shadow_270deg);
232                     break;
233                 default:
234                     break;
235             }
236         } else {
237             mLayoutParameter.gravity = Gravity.BOTTOM;
238             mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT;
239             mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT;
240             mLayout.setBackgroundResource(R.drawable.shadow_0deg);
241         }
242 
243         // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom
244         // navigation bar window.
245         updateLayoutByWindowInsetsIfNeeded();
246         mLayout.setOnApplyWindowInsetsListener(
247                 (view, insets) -> {
248                     if (updateLayoutByWindowInsetsIfNeeded()) {
249                         mWindowManager.updateViewLayout(mLayout, mLayoutParameter);
250                     }
251                     return view.onApplyWindowInsets(insets);
252                 });
253     }
254 
255     /**
256      * Returns {@code true} if the a11y menu layout params
257      * should be updated by {@link WindowManager} immediately due to window insets change.
258      * This method adjusts the layout position and size to
259      * make a11y menu not to overlap navigation bar window.
260      */
updateLayoutByWindowInsetsIfNeeded()261     private boolean updateLayoutByWindowInsetsIfNeeded() {
262         boolean shouldUpdateLayout = false;
263         WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
264         Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
265                 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
266         int xOffset = max(windowInsets.left, windowInsets.right);
267         int yOffset = windowInsets.bottom;
268         Rect windowBound = windowMetrics.getBounds();
269         if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) {
270             mLayoutParameter.x = xOffset;
271             mLayoutParameter.y = yOffset;
272             shouldUpdateLayout = true;
273         }
274         // for gestural navigation mode and the landscape mode,
275         // the layout height should be decreased by system bar
276         // and display cutout inset to fit the new
277         // frame size that doesn't overlap the navigation bar window.
278         int orientation = mService.getResources().getConfiguration().orientation;
279         if (mLayout.getHeight() != mLayoutParameter.height
280                 && orientation == Configuration.ORIENTATION_LANDSCAPE) {
281             mLayoutParameter.height = windowBound.height() - yOffset;
282             shouldUpdateLayout = true;
283         }
284         return shouldUpdateLayout;
285     }
286 
287     /**
288      * Gets the current page index when device configuration changed. {@link
289      * AccessibilityMenuService#onConfigurationChanged(Configuration)}
290      *
291      * @return the current index of the ViewPager.
292      */
getPageIndex()293     public int getPageIndex() {
294         if (mA11yMenuViewPager != null) {
295             return mA11yMenuViewPager.mViewPager.getCurrentItem();
296         }
297         return A11yMenuViewPager.DEFAULT_PAGE_INDEX;
298     }
299 
300     /**
301      * Hides a11y menu layout. And return if layout visibility has been changed.
302      *
303      * @return {@code true} layout visibility is toggled off; {@code false} is unchanged
304      */
hideMenu()305     public boolean hideMenu() {
306         if (mLayout.getVisibility() == View.VISIBLE) {
307             mLayout.setVisibility(View.GONE);
308             return true;
309         }
310         return false;
311     }
312 
313     /** Toggles a11y menu layout visibility. */
toggleVisibility()314     public void toggleVisibility() {
315         mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE);
316     }
317 
318     /** Shows hint text on a minimal Snackbar-like text view. */
showSnackbar(String text)319     public void showSnackbar(String text) {
320         final int animationDurationMs = 300;
321         final int timeoutDurationMs = mAccessibilityManager.getRecommendedTimeoutMillis(2000,
322                 AccessibilityManager.FLAG_CONTENT_TEXT);
323 
324         final TextView snackbar = mLayout.findViewById(R.id.snackbar);
325         if (snackbar == null) {
326             return;
327         }
328         snackbar.setText(text);
329         if (com.android.systemui.accessibility.accessibilitymenu
330                 .Flags.a11yMenuSnackbarLiveRegion()) {
331             snackbar.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
332         }
333 
334         // Remove any existing fade-out animation before starting any new animations.
335         mHandler.removeCallbacksAndMessages(null);
336 
337         if (snackbar.getVisibility() != View.VISIBLE) {
338             snackbar.setAlpha(0f);
339             snackbar.setVisibility(View.VISIBLE);
340             snackbar.animate().alpha(1f).setDuration(animationDurationMs).setListener(null);
341         }
342         mHandler.postDelayed(() -> snackbar.animate().alpha(0f).setDuration(
343                 animationDurationMs).setListener(
344                 new AnimatorListenerAdapter() {
345                         @Override
346                         public void onAnimationEnd(@NonNull Animator animation) {
347                             snackbar.setVisibility(View.GONE);
348                         }
349                     }), timeoutDurationMs);
350     }
351 }
352