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