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