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 android.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.Insets;
22 import android.util.DisplayMetrics;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
27 import android.view.WindowInsets;
28 import android.view.WindowManager;
29 import android.view.WindowMetrics;
30 import android.widget.GridView;
31 
32 import androidx.viewpager.widget.ViewPager;
33 
34 import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
35 import com.android.systemui.accessibility.accessibilitymenu.R;
36 import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
37 import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
38 import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * This class handles UI for viewPager and footer.
45  * It displays grid pages containing all shortcuts in viewPager,
46  * and handles the click events from footer to switch between pages.
47  */
48 public class A11yMenuViewPager {
49 
50     /** The default index of the ViewPager. */
51     public static final int DEFAULT_PAGE_INDEX = 0;
52 
53     /**
54      * The class holds the static parameters for grid view when large button settings is on/off.
55      */
56     public static final class GridViewParams {
57         /** Total shortcuts count in the grid view when large button settings is off. */
58         public static final int GRID_ITEM_COUNT = 9;
59 
60         /** The number of columns in the grid view when large button settings is off. */
61         public static final int GRID_COLUMN_COUNT = 3;
62 
63         /** Total shortcuts count in the grid view when large button settings is on. */
64         public static final int LARGE_GRID_ITEM_COUNT = 4;
65 
66         /** The number of columns in the grid view when large button settings is on. */
67         public static final int LARGE_GRID_COLUMN_COUNT = 2;
68 
69         /**
70          * Returns the number of items in the grid view.
71          *
72          * @param context The parent context
73          * @return Grid item count
74          */
getGridItemCount(Context context)75         public static int getGridItemCount(Context context) {
76             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
77                    ? LARGE_GRID_ITEM_COUNT
78                    : GRID_ITEM_COUNT;
79         }
80 
81         /**
82          * Returns the number of columns in the grid view.
83          *
84          * @param context The parent context
85          * @return Grid column count
86          */
getGridColumnCount(Context context)87         public static int getGridColumnCount(Context context) {
88             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
89                    ? LARGE_GRID_COLUMN_COUNT
90                    : GRID_COLUMN_COUNT;
91         }
92 
93         /**
94          * Returns the number of rows in the grid view.
95          *
96          * @param context The parent context
97          * @return Grid row count
98          */
getGridRowCount(Context context)99         public static int getGridRowCount(Context context) {
100             return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
101                    ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT)
102                    : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT);
103         }
104 
105         /**
106          * Separates a provided list of accessibility shortcuts into multiple sub-lists.
107          * Does not modify the original list.
108          *
109          * @param pageItemCount The maximum size of an individual sub-list.
110          * @param shortcutList The list of shortcuts to be separated into sub-lists.
111          * @return A list of shortcut sub-lists.
112          */
generateShortcutSubLists( int pageItemCount, List<A11yMenuShortcut> shortcutList)113         public static List<List<A11yMenuShortcut>> generateShortcutSubLists(
114                 int pageItemCount, List<A11yMenuShortcut> shortcutList) {
115             int start = 0;
116             int end;
117             int shortcutListSize = shortcutList.size();
118             List<List<A11yMenuShortcut>> subLists = new ArrayList<>();
119             while (start < shortcutListSize) {
120                 end = Math.min(start + pageItemCount, shortcutListSize);
121                 subLists.add(shortcutList.subList(start, end));
122                 start = end;
123             }
124             return subLists;
125         }
126 
GridViewParams()127         private GridViewParams() {}
128     }
129 
130     private final AccessibilityMenuService mService;
131 
132     /**
133      * The pager widget, which handles animation and allows swiping horizontally to access previous
134      * and next gridView pages.
135      */
136     protected ViewPager mViewPager;
137 
138     private ViewPagerAdapter<GridView> mViewPagerAdapter;
139     private final List<GridView> mGridPageList = new ArrayList<>();
140 
141     /** The footer, which provides buttons to switch between pages */
142     protected A11yMenuFooter mA11yMenuFooter;
143 
144     /** The shortcut list intended to show in grid pages of viewPager */
145     private List<A11yMenuShortcut> mA11yMenuShortcutList;
146 
147     /** The container layout for a11y menu. */
148     private ViewGroup mA11yMenuLayout;
149 
150     /** Display context for inflating views. */
151     private Context mDisplayContext;
152 
A11yMenuViewPager(AccessibilityMenuService service, Context displayContext)153     public A11yMenuViewPager(AccessibilityMenuService service, Context displayContext) {
154         this.mService = service;
155         this.mDisplayContext = displayContext;
156     }
157 
158     /**
159      * Configures UI for view pager and footer.
160      *
161      * @param a11yMenuLayout the container layout for a11y menu
162      * @param shortcutDataList the data list need to show in view pager
163      * @param pageIndex the index of ViewPager to show
164      */
configureViewPagerAndFooter( ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex)165     public void configureViewPagerAndFooter(
166             ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) {
167         this.mA11yMenuLayout = a11yMenuLayout;
168         mA11yMenuShortcutList = shortcutDataList;
169         initViewPager();
170         initChildPage();
171         mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks);
172         updateFooterState();
173         registerOnGlobalLayoutListener();
174         goToPage(pageIndex);
175     }
176 
177     /** Initializes viewPager and its adapter. */
initViewPager()178     private void initViewPager() {
179         mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager);
180         mViewPagerAdapter = new ViewPagerAdapter<>();
181         mViewPager.setAdapter(mViewPagerAdapter);
182         mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
183         mViewPager.addOnPageChangeListener(
184                 new ViewPager.OnPageChangeListener() {
185                     @Override
186                     public void onPageScrollStateChanged(int state) {}
187 
188                     @Override
189                     public void onPageScrolled(
190                             int position, float positionOffset, int positionOffsetPixels) {}
191 
192                     @Override
193                     public void onPageSelected(int position) {
194                         updateFooterState();
195                     }
196                 });
197     }
198 
199     /** Creates child pages of viewPager by the length of shortcuts and initializes them. */
initChildPage()200     private void initChildPage() {
201         if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) {
202             return;
203         }
204 
205         if (!mGridPageList.isEmpty()) {
206             mGridPageList.clear();
207         }
208 
209         // Generate pages by calculating # of items per grid.
210         for (List<A11yMenuShortcut> page : GridViewParams.generateShortcutSubLists(
211                 GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList)
212         ) {
213             addGridPage(page);
214         }
215 
216         mViewPagerAdapter.set(mGridPageList);
217     }
218 
addGridPage(List<A11yMenuShortcut> shortcutDataListInPage)219     private void addGridPage(List<A11yMenuShortcut> shortcutDataListInPage) {
220         LayoutInflater inflater = LayoutInflater.from(mDisplayContext);
221         View view = inflater.inflate(R.layout.grid_view, null);
222         GridView gridView = view.findViewById(R.id.gridview);
223         A11yMenuAdapter adapter = new A11yMenuAdapter(
224                 mService, mDisplayContext, shortcutDataListInPage);
225         gridView.setNumColumns(GridViewParams.getGridColumnCount(mService));
226         gridView.setAdapter(adapter);
227         mGridPageList.add(gridView);
228     }
229 
230     /** Updates footer's state by index of current page in view pager. */
updateFooterState()231     private void updateFooterState() {
232         int currentPage = mViewPager.getCurrentItem();
233         int lastPage = mViewPager.getAdapter().getCount() - 1;
234         mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0);
235         mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage);
236     }
237 
238     private void goToPage(int pageIndex) {
239         if (mViewPager == null) {
240             return;
241         }
242         if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getCount())) {
243             mViewPager.setCurrentItem(pageIndex);
244         }
245     }
246 
247     /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */
248     private void registerOnGlobalLayoutListener() {
249         mA11yMenuLayout
250                 .getViewTreeObserver()
251                 .addOnGlobalLayoutListener(
252                         new OnGlobalLayoutListener() {
253 
254                             boolean mIsFirstTime = true;
255 
256                             @Override
257                             public void onGlobalLayout() {
258                                 if (!mIsFirstTime) {
259                                     return;
260                                 }
261 
262                                 if (mGridPageList.isEmpty()) {
263                                     return;
264                                 }
265 
266                                 GridView firstGridView = mGridPageList.get(0);
267                                 if (firstGridView == null
268                                         || firstGridView.getChildAt(0) == null) {
269                                     return;
270                                 }
271 
272                                 mIsFirstTime = false;
273 
274                                 int gridItemHeight = firstGridView.getChildAt(0)
275                                                 .getMeasuredHeight();
276                                 adjustMenuUISize(gridItemHeight);
277                             }
278                         });
279     }
280 
281     /**
282      * Adjusts menu UI to fit both landscape and portrait mode.
283      *
284      * <ol>
285      *   <li>Adjust view pager's height.
286      *   <li>Adjust vertical interval between grid items.
287      *   <li>Adjust padding in view pager.
288      * </ol>
289      */
290     private void adjustMenuUISize(int gridItemHeight) {
291         final int rowsInGridView = GridViewParams.getGridRowCount(mService);
292         final int defaultMargin =
293                 (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin);
294         final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top);
295         final int displayMode = mService.getResources().getConfiguration().orientation;
296         int viewPagerHeight = mViewPager.getMeasuredHeight();
297 
298         if (displayMode == Configuration.ORIENTATION_PORTRAIT) {
299             // In portrait mode, we only need to adjust view pager's height to match its
300             // child's height.
301             viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin;
302         } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) {
303             // In landscape mode, we need to adjust view pager's height to match screen height
304             // and adjust its child too,
305             // because a11y menu layout height is limited by the screen height.
306             DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics();
307             float densityScale = (float) displayMetrics.densityDpi
308                     / DisplayMetrics.DENSITY_DEVICE_STABLE;
309             View footerLayout = mA11yMenuLayout.findViewById(R.id.footerlayout);
310             // Keeps footer window height unchanged no matter the density is changed.
311             footerLayout.getLayoutParams().height =
312                     (int) (footerLayout.getLayoutParams().height / densityScale);
313             // Adjust the view pager height for system bar and display cutout insets.
314             WindowManager windowManager = mService.getSystemService(WindowManager.class);
315             WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics();
316             Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility(
317                     WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
318             viewPagerHeight =
319                     windowMetric.getBounds().height()
320                             - footerLayout.getLayoutParams().height
321                             - windowInsets.bottom;
322             // Sets vertical interval between grid items.
323             int interval =
324                     (viewPagerHeight - topMargin - defaultMargin
325                             - (rowsInGridView * gridItemHeight))
326                             / (rowsInGridView + 1);
327             for (GridView gridView : mGridPageList) {
328                 gridView.setVerticalSpacing(interval);
329             }
330 
331             // Sets padding to view pager.
332             final int finalMarginTop = interval + topMargin;
333             mViewPager.setPadding(defaultMargin, finalMarginTop, defaultMargin, defaultMargin);
334         }
335         final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
336         layoutParams.height = viewPagerHeight;
337         mViewPager.setLayoutParams(layoutParams);
338     }
339 
340     /** Callback object to handle click events from A11yMenuFooter */
341     protected A11yMenuFooterCallBack mFooterCallbacks =
342             new A11yMenuFooterCallBack() {
343                 @Override
344                 public void onLeftButtonClicked() {
345                     // Moves to previous page.
346                     int targetPage = mViewPager.getCurrentItem() - 1;
347                     goToPage(targetPage);
348                     updateFooterState();
349                 }
350 
351                 @Override
352                 public void onRightButtonClicked() {
353                     // Moves to next page.
354                     int targetPage = mViewPager.getCurrentItem() + 1;
355                     goToPage(targetPage);
356                     updateFooterState();
357                 }
358             };
359 }
360