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