• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 if (isUninstallKeyChord(e)) {
254             matrix = FocusLogic.createSparseMatrix(iconLayout);
255             if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
256                 UninstallDropTarget.startUninstallActivity(launcher, itemInfo);
257             }
258         } else if (isDeleteKeyChord(e)) {
259             matrix = FocusLogic.createSparseMatrix(iconLayout);
260             launcher.removeItem(v, itemInfo, true /* deleteFromDb */);
261         } else {
262             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
263             // matrix extended with hotseat.
264             matrix = FocusLogic.createSparseMatrix(hotseatLayout);
265             parent = hotseatParent;
266         }
267 
268         // Process the focus.
269         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
270                 pageCount, Utilities.isRtl(v.getResources()));
271 
272         View newIcon = null;
273         switch (newIconIndex) {
274             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
275                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
276                 newIcon = parent.getChildAt(0);
277                 // TODO(hyunyoungs): handle cases where the child is not an icon but
278                 // a folder or a widget.
279                 workspace.snapToPage(pageIndex + 1);
280                 break;
281             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
282                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
283                 newIcon = parent.getChildAt(0);
284                 // TODO(hyunyoungs): handle cases where the child is not an icon but
285                 // a folder or a widget.
286                 workspace.snapToPage(pageIndex - 1);
287                 break;
288             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
289                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
290                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
291                 // TODO(hyunyoungs): handle cases where the child is not an icon but
292                 // a folder or a widget.
293                 workspace.snapToPage(pageIndex - 1);
294                 break;
295             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
296             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
297                 // Go to the previous page but keep the focus on the same hotseat icon.
298                 workspace.snapToPage(pageIndex - 1);
299                 // If the page we are going to is fullscreen, have it take the focus from hotseat.
300                 CellLayout prevPage = (CellLayout) workspace.getPageAt(pageIndex - 1);
301                 boolean isPrevPageFullscreen = ((CellLayout.LayoutParams) prevPage
302                         .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen;
303                 if (isPrevPageFullscreen) {
304                     workspace.getPageAt(pageIndex - 1).requestFocus();
305                 }
306                 break;
307             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
308             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
309                 // Go to the next page but keep the focus on the same hotseat icon.
310                 workspace.snapToPage(pageIndex + 1);
311                 // If the page we are going to is fullscreen, have it take the focus from hotseat.
312                 CellLayout nextPage = (CellLayout) workspace.getPageAt(pageIndex + 1);
313                 boolean isNextPageFullscreen = ((CellLayout.LayoutParams) nextPage
314                         .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen;
315                 if (isNextPageFullscreen) {
316                     workspace.getPageAt(pageIndex + 1).requestFocus();
317                 }
318                 break;
319         }
320         if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
321             newIconIndex -= iconParent.getChildCount();
322         }
323         if (parent != null) {
324             if (newIcon == null && newIconIndex >= 0) {
325                 newIcon = parent.getChildAt(newIconIndex);
326             }
327             if (newIcon != null) {
328                 newIcon.requestFocus();
329                 playSoundEffect(keyCode, v);
330             }
331         }
332         return consume;
333     }
334 
335     /**
336      * Handles key events in a workspace containing icons.
337      */
handleIconKeyEvent(View v, int keyCode, KeyEvent e)338     static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
339         boolean consume = FocusLogic.shouldConsume(keyCode);
340         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
341             return consume;
342         }
343 
344         Launcher launcher = Launcher.getLauncher(v.getContext());
345         DeviceProfile profile = launcher.getDeviceProfile();
346 
347         if (DEBUG) {
348             Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
349                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
350         }
351 
352         // Initialize the variables.
353         ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
354         CellLayout iconLayout = (CellLayout) parent.getParent();
355         final Workspace workspace = (Workspace) iconLayout.getParent();
356         final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
357         final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.drop_target_bar);
358         final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
359 
360         final ItemInfo itemInfo = (ItemInfo) v.getTag();
361         final int iconIndex = parent.indexOfChild(v);
362         final int pageIndex = workspace.indexOfChild(iconLayout);
363         final int pageCount = workspace.getChildCount();
364 
365         CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
366         ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
367         int[][] matrix;
368 
369         // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
370         // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
371         // with the hotseat.
372         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
373             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
374         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
375                 profile.isVerticalBarLayout()) {
376             matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile);
377         } else if (isUninstallKeyChord(e)) {
378             matrix = FocusLogic.createSparseMatrix(iconLayout);
379             if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
380                 UninstallDropTarget.startUninstallActivity(launcher, itemInfo);
381             }
382         } else if (isDeleteKeyChord(e)) {
383             matrix = FocusLogic.createSparseMatrix(iconLayout);
384             launcher.removeItem(v, itemInfo, true /* deleteFromDb */);
385         } else {
386             matrix = FocusLogic.createSparseMatrix(iconLayout);
387         }
388 
389         // Process the focus.
390         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex,
391                 pageCount, Utilities.isRtl(v.getResources()));
392         boolean isRtl = Utilities.isRtl(v.getResources());
393         View newIcon = null;
394         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex);
395         switch (newIconIndex) {
396             case FocusLogic.NOOP:
397                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
398                     newIcon = tabs;
399                 }
400                 break;
401             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
402             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
403                 int newPageIndex = pageIndex - 1;
404                 if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
405                     newPageIndex = pageIndex + 1;
406                 }
407                 int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
408                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
409                 if (parent != null) {
410                     iconLayout = (CellLayout) parent.getParent();
411                     matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout,
412                             iconLayout.getCountX(), row);
413                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
414                             newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
415                     if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
416                         newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
417                                 isRtl);
418                     } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
419                         newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
420                                 isRtl);
421                     } else {
422                         newIcon = parent.getChildAt(newIconIndex);
423                     }
424                 }
425                 break;
426             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
427                 workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
428                 newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
429                 if (newIcon == null) {
430                     // Check the hotseat if no focusable item was found on the workspace.
431                     newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
432                     workspace.snapToPage(pageIndex - 1);
433                 }
434                 break;
435             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
436                 newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl);
437                 break;
438             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
439                 newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl);
440                 break;
441             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
442             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
443                 newPageIndex = pageIndex + 1;
444                 if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
445                     newPageIndex = pageIndex - 1;
446                 }
447                 row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
448                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
449                 if (parent != null) {
450                     iconLayout = (CellLayout) parent.getParent();
451                     matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row);
452                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT,
453                             newPageIndex, pageCount, Utilities.isRtl(v.getResources()));
454                     if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
455                         newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
456                                 isRtl);
457                     } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
458                         newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
459                                 isRtl);
460                     } else {
461                         newIcon = parent.getChildAt(newIconIndex);
462                     }
463                 }
464                 break;
465             case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
466                 newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
467                 if (newIcon == null) {
468                     // Check the hotseat if no focusable item was found on the workspace.
469                     newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
470                 }
471                 break;
472             case FocusLogic.CURRENT_PAGE_LAST_ITEM:
473                 newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
474                 if (newIcon == null) {
475                     // Check the hotseat if no focusable item was found on the workspace.
476                     newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl);
477                 }
478                 break;
479             default:
480                 // current page, some item.
481                 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
482                     newIcon = parent.getChildAt(newIconIndex);
483                 } else if (parent.getChildCount() <= newIconIndex &&
484                         newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
485                     newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
486                 }
487                 break;
488         }
489         if (newIcon != null) {
490             newIcon.requestFocus();
491             playSoundEffect(keyCode, v);
492         }
493         return consume;
494     }
495 
496     //
497     // Helper methods.
498     //
499 
500     /**
501      * Private helper method to get the CellLayoutChildren given a CellLayout index.
502      */
getCellLayoutChildrenForIndex( ViewGroup container, int i)503     @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
504             ViewGroup container, int i) {
505         CellLayout parent = (CellLayout) container.getChildAt(i);
506         return parent.getShortcutsAndWidgets();
507     }
508 
509     /**
510      * Helper method to be used for playing sound effects.
511      */
playSoundEffect(int keyCode, View v)512     @Thunk static void playSoundEffect(int keyCode, View v) {
513         switch (keyCode) {
514             case KeyEvent.KEYCODE_DPAD_LEFT:
515                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
516                 break;
517             case KeyEvent.KEYCODE_DPAD_RIGHT:
518                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
519                 break;
520             case KeyEvent.KEYCODE_DPAD_DOWN:
521             case KeyEvent.KEYCODE_PAGE_DOWN:
522             case KeyEvent.KEYCODE_MOVE_END:
523                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
524                 break;
525             case KeyEvent.KEYCODE_DPAD_UP:
526             case KeyEvent.KEYCODE_PAGE_UP:
527             case KeyEvent.KEYCODE_MOVE_HOME:
528                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
529                 break;
530             default:
531                 break;
532         }
533     }
534 
535     /**
536      * Returns whether the key event represents a valid uninstall key chord.
537      */
isUninstallKeyChord(KeyEvent event)538     private static boolean isUninstallKeyChord(KeyEvent event) {
539         int keyCode = event.getKeyCode();
540         return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) &&
541                 event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON);
542     }
543 
544     /**
545      * Returns whether the key event represents a valid delete key chord.
546      */
isDeleteKeyChord(KeyEvent event)547     private static boolean isDeleteKeyChord(KeyEvent event) {
548         int keyCode = event.getKeyCode();
549         return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) &&
550                 event.hasModifiers(KeyEvent.META_CTRL_ON);
551     }
552 
handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl)553     private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout,
554             int pageIndex, boolean isRtl) {
555         if (pageIndex - 1 < 0) {
556             return null;
557         }
558         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
559         View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
560         if (newIcon == null) {
561             // Check the hotseat if no focusable item was found on the workspace.
562             newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl);
563             workspace.snapToPage(pageIndex - 1);
564         }
565         return newIcon;
566     }
567 
handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl)568     private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout,
569             int pageIndex, boolean isRtl) {
570         if (pageIndex + 1 >= workspace.getPageCount()) {
571             return null;
572         }
573         CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1);
574         View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
575         if (newIcon == null) {
576             // Check the hotseat if no focusable item was found on the workspace.
577             newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
578             workspace.snapToPage(pageIndex + 1);
579         }
580         return newIcon;
581     }
582 
getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl)583     private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) {
584         View icon;
585         int countX = cellLayout.getCountX();
586         for (int y = 0; y < cellLayout.getCountY(); y++) {
587             int increment = isRtl ? -1 : 1;
588             for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) {
589                 if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
590                     return icon;
591                 }
592             }
593         }
594         return null;
595     }
596 
getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout, boolean isRtl)597     private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout,
598             boolean isRtl) {
599         View icon;
600         int countX = cellLayout.getCountX();
601         for (int y = cellLayout.getCountY() - 1; y >= 0; y--) {
602             int increment = isRtl ? 1 : -1;
603             for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) {
604                 if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
605                     return icon;
606                 }
607             }
608         }
609         return null;
610     }
611 }
612