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