1 /*
2  * Copyright (C) 2015 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.launcher3;
18 
19 import android.util.Log;
20 import android.view.KeyEvent;
21 import android.view.SoundEffectConstants;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import com.android.launcher3.config.FeatureFlags;
26 import com.android.launcher3.folder.Folder;
27 import com.android.launcher3.folder.FolderPagedView;
28 import com.android.launcher3.util.FocusLogic;
29 import com.android.launcher3.util.Thunk;
30 
31 /**
32  * A keyboard listener we set on all the workspace icons.
33  */
34 class IconKeyEventListener implements View.OnKeyListener {
35     @Override
onKey(View v, int keyCode, KeyEvent event)36     public boolean onKey(View v, int keyCode, KeyEvent event) {
37         return FocusHelper.handleIconKeyEvent(v, keyCode, event);
38     }
39 }
40 
41 /**
42  * A keyboard listener we set on all the hotseat buttons.
43  */
44 class HotseatIconKeyEventListener implements View.OnKeyListener {
45     @Override
onKey(View v, int keyCode, KeyEvent event)46     public boolean onKey(View v, int keyCode, KeyEvent event) {
47         return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
48     }
49 }
50 
51 /**
52  * A keyboard listener we set on full screen pages (e.g. custom content).
53  */
54 class FullscreenKeyEventListener implements View.OnKeyListener {
55     @Override
onKey(View v, int keyCode, KeyEvent event)56     public boolean onKey(View v, int keyCode, KeyEvent event) {
57         if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
58                 || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_PAGE_UP) {
59             // Handle the key event just like a workspace icon would in these cases. In this case,
60             // it will basically act as if there is a single icon in the top left (so you could
61             // think of the fullscreen page as a focusable fullscreen widget).
62             return FocusHelper.handleIconKeyEvent(v, keyCode, event);
63         }
64         return false;
65     }
66 }
67 
68 /**
69  * TODO: Reevaluate if this is still required
70  */
71 public class FocusHelper {
72 
73     private static final String TAG = "FocusHelper";
74     private static final boolean DEBUG = false;
75 
76     /**
77      * Handles key events in paged folder.
78      */
79     public static class PagedFolderKeyEventListener implements View.OnKeyListener {
80 
81         private final Folder mFolder;
82 
PagedFolderKeyEventListener(Folder folder)83         public PagedFolderKeyEventListener(Folder folder) {
84             mFolder = folder;
85         }
86 
87         @Override
onKey(View v, int keyCode, KeyEvent e)88         public boolean onKey(View v, int keyCode, KeyEvent e) {
89             boolean consume = FocusLogic.shouldConsume(keyCode);
90             if (e.getAction() == KeyEvent.ACTION_UP) {
91                 return consume;
92             }
93             if (DEBUG) {
94                 Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
95                         KeyEvent.keyCodeToString(keyCode)));
96             }
97 
98             if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
99                 if (FeatureFlags.IS_DOGFOOD_BUILD) {
100                     throw new IllegalStateException("Parent of the focused item is not supported.");
101                 } else {
102                     return false;
103                 }
104             }
105 
106             // Initialize variables.
107             final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
108             final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
109 
110             final int iconIndex = itemContainer.indexOfChild(v);
111             final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
112 
113             final int pageIndex = pagedView.indexOfChild(cellLayout);
114             final int pageCount = pagedView.getPageCount();
115             final boolean isLayoutRtl = Utilities.isRtl(v.getResources());
116 
117             int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
118             // Process focus.
119             int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
120                     pageCount, isLayoutRtl);
121             if (newIconIndex == FocusLogic.NOOP) {
122                 handleNoopKey(keyCode, v);
123                 return consume;
124             }
125             ShortcutAndWidgetContainer newParent = null;
126             View child = null;
127 
128             switch (newIconIndex) {
129                 case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
130                 case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
131                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
132                     if (newParent != null) {
133                         int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
134                         pagedView.snapToPage(pageIndex - 1);
135                         child = newParent.getChildAt(
136                                 ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
137                                     ^ newParent.invertLayoutHorizontally()) ? 0 : matrix.length - 1,
138                                 row);
139                     }
140                     break;
141                 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
142                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
143                     if (newParent != null) {
144                         pagedView.snapToPage(pageIndex - 1);
145                         child = newParent.getChildAt(0, 0);
146                     }
147                     break;
148                 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
149                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
150                     if (newParent != null) {
151                         pagedView.snapToPage(pageIndex - 1);
152                         child = newParent.getChildAt(matrix.length - 1, matrix[0].length - 1);
153                     }
154                     break;
155                 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
156                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
157                     if (newParent != null) {
158                         pagedView.snapToPage(pageIndex + 1);
159                         child = newParent.getChildAt(0, 0);
160                     }
161                     break;
162                 case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
163                 case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
164                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
165                     if (newParent != null) {
166                         pagedView.snapToPage(pageIndex + 1);
167                         child = FocusLogic.getAdjacentChildInNextFolderPage(
168                                 newParent, v, newIconIndex);
169                     }
170                     break;
171                 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
172                     child = cellLayout.getChildAt(0, 0);
173                     break;
174                 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
175                     child = pagedView.getLastItem();
176                     break;
177                 default: // Go to some item on the current page.
178                     child = itemContainer.getChildAt(newIconIndex);
179                     break;
180             }
181             if (child != null) {
182                 child.requestFocus();
183                 playSoundEffect(keyCode, v);
184             } else {
185                 handleNoopKey(keyCode, v);
186             }
187             return consume;
188         }
189 
handleNoopKey(int keyCode, View v)190         public void handleNoopKey(int keyCode, View v) {
191             if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
192                 mFolder.mFolderName.requestFocus();
193                 playSoundEffect(keyCode, v);
194             }
195         }
196     }
197 
198     /**
199      * Handles key events in the workspace hotseat (bottom of the screen).
200      * <p>Currently we don't special case for the phone UI in different orientations, even though
201      * the hotseat is on the side in landscape mode. This is to ensure that accessibility
202      * consistency is maintained across rotations.
203      */
handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e)204     static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
205         boolean consume = FocusLogic.shouldConsume(keyCode);
206         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
207             return consume;
208         }
209 
210         final Launcher launcher = Launcher.getLauncher(v.getContext());
211         final DeviceProfile profile = launcher.getDeviceProfile();
212 
213         if (DEBUG) {
214             Log.v(TAG, String.format(
215                     "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
216                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
217         }
218 
219         // Initialize the variables.
220         final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
221         final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
222         final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
223 
224         final ItemInfo itemInfo = (ItemInfo) v.getTag();
225         int pageIndex = workspace.getNextPage();
226         int pageCount = workspace.getChildCount();
227         int iconIndex = hotseatParent.indexOfChild(v);
228         int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
229                 .getChildAt(iconIndex).getLayoutParams()).cellX;
230 
231         final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
232         if (iconLayout == null) {
233             // This check is to guard against cases where key strokes rushes in when workspace
234             // child creation/deletion is still in flux. (e.g., during drop or fling
235             // animation.)
236             return consume;
237         }
238         final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
239 
240         ViewGroup parent = null;
241         int[][] matrix = null;
242 
243         if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
244                 !profile.isVerticalBarLayout()) {
245             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
246             iconIndex += iconParent.getChildCount();
247             parent = iconParent;
248         } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
249                 profile.isVerticalBarLayout()) {
250             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
251             iconIndex += iconParent.getChildCount();
252             parent = iconParent;
253         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
254                 profile.isVerticalBarLayout()) {
255             keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
256         } else {
257             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
258             // matrix extended with hotseat.
259             matrix = FocusLogic.createSparseMatrix(hotseatLayout);
260             parent = hotseatParent;
261         }
262 
263         // Process the focus.
264         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
265                 pageCount, Utilities.isRtl(v.getResources()));
266 
267         View newIcon = null;
268         switch (newIconIndex) {
269             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
270                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
271                 newIcon = parent.getChildAt(0);
272                 // TODO(hyunyoungs): handle cases where the child is not an icon but
273                 // a folder or a widget.
274                 workspace.snapToPage(pageIndex + 1);
275                 break;
276             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
277                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
278                 newIcon = parent.getChildAt(0);
279                 // TODO(hyunyoungs): handle cases where the child is not an icon but
280                 // a folder or a widget.
281                 workspace.snapToPage(pageIndex - 1);
282                 break;
283             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
284                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
285                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
286                 // TODO(hyunyoungs): handle cases where the child is not an icon but
287                 // a folder or a widget.
288                 workspace.snapToPage(pageIndex - 1);
289                 break;
290             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
291             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
292                 // Go to the previous page but keep the focus on the same hotseat icon.
293                 workspace.snapToPage(pageIndex - 1);
294                 break;
295             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
296             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
297                 // Go to the next page but keep the focus on the same hotseat icon.
298                 workspace.snapToPage(pageIndex + 1);
299                 break;
300         }
301         if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
302             newIconIndex -= iconParent.getChildCount();
303         }
304         if (parent != null) {
305             if (newIcon == null && newIconIndex >= 0) {
306                 newIcon = parent.getChildAt(newIconIndex);
307             }
308             if (newIcon != null) {
309                 newIcon.requestFocus();
310                 playSoundEffect(keyCode, v);
311             }
312         }
313         return consume;
314     }
315 
316     /**
317      * Handles key events in a workspace containing icons.
318      */
handleIconKeyEvent(View v, int keyCode, KeyEvent e)319     static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
320         boolean consume = FocusLogic.shouldConsume(keyCode);
321         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
322             return consume;
323         }
324 
325         Launcher launcher = Launcher.getLauncher(v.getContext());
326         DeviceProfile profile = launcher.getDeviceProfile();
327 
328         if (DEBUG) {
329             Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
330                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
331         }
332 
333         // Initialize the variables.
334         ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
335         CellLayout iconLayout = (CellLayout) parent.getParent();
336         final Workspace workspace = (Workspace) iconLayout.getParent();
337         final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
338         final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.drop_target_bar);
339         final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
340 
341         final ItemInfo itemInfo = (ItemInfo) v.getTag();
342         final int iconIndex = parent.indexOfChild(v);
343         final int pageIndex = workspace.indexOfChild(iconLayout);
344         final int pageCount = workspace.getChildCount();
345 
346         CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
347         ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
348         int[][] matrix;
349 
350         // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
351         // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
352         // with the hotseat.
353         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
354             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
355         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
356                 profile.isVerticalBarLayout()) {
357             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
358         } else {
359             matrix = FocusLogic.createSparseMatrix(iconLayout);
360         }
361 
362         // Process the focus.
363         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
364                 pageCount, Utilities.isRtl(v.getResources()));
365         boolean isRtl = Utilities.isRtl(v.getResources());
366         View newIcon = null;
367         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex);
368         switch (newIconIndex) {
369             case FocusLogic.NOOP:
370                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
371                     newIcon = tabs;
372                 }
373                 break;
374             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
375             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
376                 int newPageIndex = pageIndex - 1;
377                 if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
378                     newPageIndex = pageIndex + 1;
379                 }
380                 int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
381                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
382                 if (parent != null) {
383                     iconLayout = (CellLayout) parent.getParent();
384                     matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout,
385                             iconLayout.getCountX(), row);
386                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
387                             newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
388                     if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
389                         newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
390                                 isRtl);
391                     } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
392                         newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
393                                 isRtl);
394                     } else {
395                         newIcon = parent.getChildAt(newIconIndex);
396                     }
397                 }
398                 break;
399             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
400                 workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
401                 newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
402                 if (newIcon == null) {
403                     // Check the hotseat if no focusable item was found on the workspace.
404                     newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
405                     workspace.snapToPage(pageIndex - 1);
406                 }
407                 break;
408             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
409                 newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl);
410                 break;
411             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
412                 newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl);
413                 break;
414             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
415             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
416                 newPageIndex = pageIndex + 1;
417                 if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
418                     newPageIndex = pageIndex - 1;
419                 }
420                 row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
421                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
422                 if (parent != null) {
423                     iconLayout = (CellLayout) parent.getParent();
424                     matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row);
425                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
426                             newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
427                     if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
428                         newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
429                                 isRtl);
430                     } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
431                         newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
432                                 isRtl);
433                     } else {
434                         newIcon = parent.getChildAt(newIconIndex);
435                     }
436                 }
437                 break;
438             case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
439                 newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
440                 if (newIcon == null) {
441                     // Check the hotseat if no focusable item was found on the workspace.
442                     newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
443                 }
444                 break;
445             case FocusLogic.CURRENT_PAGE_LAST_ITEM:
446                 newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
447                 if (newIcon == null) {
448                     // Check the hotseat if no focusable item was found on the workspace.
449                     newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl);
450                 }
451                 break;
452             default:
453                 // current page, some item.
454                 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
455                     newIcon = parent.getChildAt(newIconIndex);
456                 } else if (parent.getChildCount() <= newIconIndex &&
457                         newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
458                     newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
459                 }
460                 break;
461         }
462         if (newIcon != null) {
463             newIcon.requestFocus();
464             playSoundEffect(keyCode, v);
465         }
466         return consume;
467     }
468 
469     //
470     // Helper methods.
471     //
472 
473     /**
474      * Private helper method to get the CellLayoutChildren given a CellLayout index.
475      */
getCellLayoutChildrenForIndex( ViewGroup container, int i)476     @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
477             ViewGroup container, int i) {
478         CellLayout parent = (CellLayout) container.getChildAt(i);
479         return parent.getShortcutsAndWidgets();
480     }
481 
482     /**
483      * Helper method to be used for playing sound effects.
484      */
playSoundEffect(int keyCode, View v)485     @Thunk static void playSoundEffect(int keyCode, View v) {
486         switch (keyCode) {
487             case KeyEvent.KEYCODE_DPAD_LEFT:
488                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
489                 break;
490             case KeyEvent.KEYCODE_DPAD_RIGHT:
491                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
492                 break;
493             case KeyEvent.KEYCODE_DPAD_DOWN:
494             case KeyEvent.KEYCODE_PAGE_DOWN:
495             case KeyEvent.KEYCODE_MOVE_END:
496                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
497                 break;
498             case KeyEvent.KEYCODE_DPAD_UP:
499             case KeyEvent.KEYCODE_PAGE_UP:
500             case KeyEvent.KEYCODE_MOVE_HOME:
501                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
502                 break;
503             default:
504                 break;
505         }
506     }
507 
handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl)508     private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout,
509             int pageIndex, boolean isRtl) {
510         if (pageIndex - 1 < 0) {
511             return null;
512         }
513         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
514         View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
515         if (newIcon == null) {
516             // Check the hotseat if no focusable item was found on the workspace.
517             newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl);
518             workspace.snapToPage(pageIndex - 1);
519         }
520         return newIcon;
521     }
522 
handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl)523     private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout,
524             int pageIndex, boolean isRtl) {
525         if (pageIndex + 1 >= workspace.getPageCount()) {
526             return null;
527         }
528         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1);
529         View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
530         if (newIcon == null) {
531             // Check the hotseat if no focusable item was found on the workspace.
532             newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
533             workspace.snapToPage(pageIndex + 1);
534         }
535         return newIcon;
536     }
537 
getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl)538     private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) {
539         View icon;
540         int countX = cellLayout.getCountX();
541         for (int y = 0; y < cellLayout.getCountY(); y++) {
542             int increment = isRtl ? -1 : 1;
543             for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) {
544                 if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
545                     return icon;
546                 }
547             }
548         }
549         return null;
550     }
551 
getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout, boolean isRtl)552     private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout,
553             boolean isRtl) {
554         View icon;
555         int countX = cellLayout.getCountX();
556         for (int y = cellLayout.getCountY() - 1; y >= 0; y--) {
557             int increment = isRtl ? 1 : -1;
558             for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) {
559                 if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
560                     return icon;
561                 }
562             }
563         }
564         return null;
565     }
566 }
567