• 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.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 public class FocusHelper {
49 
50     private static final String TAG = "FocusHelper";
51     private static final boolean DEBUG = false;
52 
53     /**
54      * Handles key events in paged folder.
55      */
56     public static class PagedFolderKeyEventListener implements View.OnKeyListener {
57 
58         private final Folder mFolder;
59 
PagedFolderKeyEventListener(Folder folder)60         public PagedFolderKeyEventListener(Folder folder) {
61             mFolder = folder;
62         }
63 
64         @Override
onKey(View v, int keyCode, KeyEvent e)65         public boolean onKey(View v, int keyCode, KeyEvent e) {
66             boolean consume = FocusLogic.shouldConsume(keyCode);
67             if (e.getAction() == KeyEvent.ACTION_UP) {
68                 return consume;
69             }
70             if (DEBUG) {
71                 Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
72                         KeyEvent.keyCodeToString(keyCode)));
73             }
74 
75 
76             if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
77                 if (LauncherAppState.isDogfoodBuild()) {
78                     throw new IllegalStateException("Parent of the focused item is not supported.");
79                 } else {
80                     return false;
81                 }
82             }
83 
84             // Initialize variables.
85             final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
86             final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
87             final int countX = cellLayout.getCountX();
88             final int countY = cellLayout.getCountY();
89 
90             final int iconIndex = itemContainer.indexOfChild(v);
91             final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
92 
93             final int pageIndex = pagedView.indexOfChild(cellLayout);
94             final int pageCount = pagedView.getPageCount();
95             final boolean isLayoutRtl = Utilities.isRtl(v.getResources());
96 
97             int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
98             // Process focus.
99             int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
100                     countY, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl);
101             if (newIconIndex == FocusLogic.NOOP) {
102                 handleNoopKey(keyCode, v);
103                 return consume;
104             }
105             ShortcutAndWidgetContainer newParent = null;
106             View child = null;
107 
108             switch (newIconIndex) {
109                 case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
110                 case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
111                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
112                     if (newParent != null) {
113                         int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
114                         pagedView.snapToPage(pageIndex - 1);
115                         child = newParent.getChildAt(
116                                 ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
117                                     ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row);
118                     }
119                     break;
120                 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
121                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
122                     if (newParent != null) {
123                         pagedView.snapToPage(pageIndex - 1);
124                         child = newParent.getChildAt(0, 0);
125                     }
126                     break;
127                 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
128                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
129                     if (newParent != null) {
130                         pagedView.snapToPage(pageIndex - 1);
131                         child = newParent.getChildAt(countX - 1, countY - 1);
132                     }
133                     break;
134                 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
135                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
136                     if (newParent != null) {
137                         pagedView.snapToPage(pageIndex + 1);
138                         child = newParent.getChildAt(0, 0);
139                     }
140                     break;
141                 case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
142                 case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
143                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
144                     if (newParent != null) {
145                         pagedView.snapToPage(pageIndex + 1);
146                         child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex);
147                     }
148                     break;
149                 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
150                     child = cellLayout.getChildAt(0, 0);
151                     break;
152                 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
153                     child = pagedView.getLastItem();
154                     break;
155                 default: // Go to some item on the current page.
156                     child = itemContainer.getChildAt(newIconIndex);
157                     break;
158             }
159             if (child != null) {
160                 child.requestFocus();
161                 playSoundEffect(keyCode, v);
162             } else {
163                 handleNoopKey(keyCode, v);
164             }
165             return consume;
166         }
167 
handleNoopKey(int keyCode, View v)168         public void handleNoopKey(int keyCode, View v) {
169             if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
170                 mFolder.mFolderName.requestFocus();
171                 playSoundEffect(keyCode, v);
172             }
173         }
174     }
175 
176     /**
177      * Handles key events in the workspace hot seat (bottom of the screen).
178      * <p>Currently we don't special case for the phone UI in different orientations, even though
179      * the hotseat is on the side in landscape mode. This is to ensure that accessibility
180      * consistency is maintained across rotations.
181      */
handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e)182     static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
183         boolean consume = FocusLogic.shouldConsume(keyCode);
184         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
185             return consume;
186         }
187 
188         DeviceProfile profile = ((Launcher) v.getContext()).getDeviceProfile();
189 
190         if (DEBUG) {
191             Log.v(TAG, String.format(
192                     "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
193                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
194         }
195 
196         // Initialize the variables.
197         final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
198         final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
199         Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
200 
201         Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
202         int pageIndex = workspace.getNextPage();
203         int pageCount = workspace.getChildCount();
204         int countX = -1;
205         int countY = -1;
206         int iconIndex = hotseatParent.indexOfChild(v);
207         int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
208                 .getChildAt(iconIndex).getLayoutParams()).cellX;
209 
210         final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
211         if (iconLayout == null) {
212             // This check is to guard against cases where key strokes rushes in when workspace
213             // child creation/deletion is still in flux. (e.g., during drop or fling
214             // animation.)
215             return consume;
216         }
217         final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
218 
219         ViewGroup parent = null;
220         int[][] matrix = null;
221 
222         if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
223                 !profile.isVerticalBarLayout()) {
224             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
225                     true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
226                     iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
227             iconIndex += iconParent.getChildCount();
228             countX = iconLayout.getCountX();
229             countY = iconLayout.getCountY() + hotseatLayout.getCountY();
230             parent = iconParent;
231         } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
232                 profile.isVerticalBarLayout()) {
233             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
234                     false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
235                     iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
236             iconIndex += iconParent.getChildCount();
237             countX = iconLayout.getCountX() + hotseatLayout.getCountX();
238             countY = iconLayout.getCountY();
239             parent = iconParent;
240         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
241                 profile.isVerticalBarLayout()) {
242             keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
243         }else {
244             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
245             // matrix extended with hotseat.
246             matrix = FocusLogic.createSparseMatrix(hotseatLayout);
247             countX = hotseatLayout.getCountX();
248             countY = hotseatLayout.getCountY();
249             parent = hotseatParent;
250         }
251 
252         // Process the focus.
253         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
254                 countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources()));
255 
256         View newIcon = null;
257         if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
258             parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
259             newIcon = parent.getChildAt(0);
260             // TODO(hyunyoungs): handle cases where the child is not an icon but
261             // a folder or a widget.
262             workspace.snapToPage(pageIndex + 1);
263         }
264         if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
265             newIconIndex -= iconParent.getChildCount();
266         }
267         if (parent != null) {
268             if (newIcon == null && newIconIndex >=0) {
269                 newIcon = parent.getChildAt(newIconIndex);
270             }
271             if (newIcon != null) {
272                 newIcon.requestFocus();
273                 playSoundEffect(keyCode, v);
274             }
275         }
276         return consume;
277     }
278 
279     /**
280      * Handles key events in a workspace containing icons.
281      */
handleIconKeyEvent(View v, int keyCode, KeyEvent e)282     static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
283         boolean consume = FocusLogic.shouldConsume(keyCode);
284         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
285             return consume;
286         }
287 
288         Launcher launcher = (Launcher) v.getContext();
289         DeviceProfile profile = launcher.getDeviceProfile();
290 
291         if (DEBUG) {
292             Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
293                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
294         }
295 
296         // Initialize the variables.
297         ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
298         CellLayout iconLayout = (CellLayout) parent.getParent();
299         final Workspace workspace = (Workspace) iconLayout.getParent();
300         final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
301         final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar);
302         final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
303 
304         final int iconIndex = parent.indexOfChild(v);
305         final int pageIndex = workspace.indexOfChild(iconLayout);
306         final int pageCount = workspace.getChildCount();
307         int countX = iconLayout.getCountX();
308         int countY = iconLayout.getCountY();
309 
310         CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
311         ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
312         int[][] matrix;
313 
314         // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
315         // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
316         // with the hotseat.
317         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
318             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
319                     profile.inv.hotseatAllAppsRank,
320                     !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
321             countY = countY + 1;
322         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
323                 profile.isVerticalBarLayout()) {
324             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
325                     profile.inv.hotseatAllAppsRank,
326                     !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
327             countX = countX + 1;
328         } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
329             workspace.removeWorkspaceItem(v);
330             return consume;
331         } else {
332             matrix = FocusLogic.createSparseMatrix(iconLayout);
333         }
334 
335         // Process the focus.
336         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
337                 countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources()));
338         View newIcon = null;
339         switch (newIconIndex) {
340             case FocusLogic.NOOP:
341                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
342                     newIcon = tabs;
343                 }
344                 break;
345             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
346             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
347                 int newPageIndex = pageIndex - 1;
348                 if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
349                     newPageIndex = pageIndex + 1;
350                 }
351                 int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
352                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
353                 workspace.snapToPage(newPageIndex);
354                 if (parent != null) {
355                     workspace.snapToPage(newPageIndex);
356                     iconLayout = (CellLayout) parent.getParent();
357                     matrix = FocusLogic.createSparseMatrix(iconLayout,
358                         iconLayout.getCountX(), row);
359                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
360                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
361                             Utilities.isRtl(v.getResources()));
362                     newIcon = parent.getChildAt(newIconIndex);
363                 }
364                 break;
365             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
366                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
367                 newIcon = parent.getChildAt(0);
368                 workspace.snapToPage(pageIndex - 1);
369                 break;
370             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
371                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
372                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
373                 workspace.snapToPage(pageIndex - 1);
374                 break;
375             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
376                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
377                 newIcon = parent.getChildAt(0);
378                 workspace.snapToPage(pageIndex + 1);
379                 break;
380             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
381             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
382                 newPageIndex = pageIndex + 1;
383                 if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
384                     newPageIndex = pageIndex - 1;
385                 }
386                 workspace.snapToPage(newPageIndex);
387                 row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
388                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
389                 if (parent != null) {
390                     workspace.snapToPage(newPageIndex);
391                     iconLayout = (CellLayout) parent.getParent();
392                     matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
393                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
394                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
395                             Utilities.isRtl(v.getResources()));
396                     newIcon = parent.getChildAt(newIconIndex);
397                 }
398                 break;
399             case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
400                 newIcon = parent.getChildAt(0);
401                 break;
402             case FocusLogic.CURRENT_PAGE_LAST_ITEM:
403                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
404                 break;
405             default:
406                 // current page, some item.
407                 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
408                     newIcon = parent.getChildAt(newIconIndex);
409                 } else if (parent.getChildCount() <= newIconIndex &&
410                         newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
411                     newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
412                 }
413                 break;
414         }
415         if (newIcon != null) {
416             newIcon.requestFocus();
417             playSoundEffect(keyCode, v);
418         }
419         return consume;
420     }
421 
422     //
423     // Helper methods.
424     //
425 
426     /**
427      * Private helper method to get the CellLayoutChildren given a CellLayout index.
428      */
getCellLayoutChildrenForIndex( ViewGroup container, int i)429     @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
430             ViewGroup container, int i) {
431         CellLayout parent = (CellLayout) container.getChildAt(i);
432         return parent.getShortcutsAndWidgets();
433     }
434 
435     /**
436      * Helper method to be used for playing sound effects.
437      */
playSoundEffect(int keyCode, View v)438     @Thunk static void playSoundEffect(int keyCode, View v) {
439         switch (keyCode) {
440             case KeyEvent.KEYCODE_DPAD_LEFT:
441                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
442                 break;
443             case KeyEvent.KEYCODE_DPAD_RIGHT:
444                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
445                 break;
446             case KeyEvent.KEYCODE_DPAD_DOWN:
447             case KeyEvent.KEYCODE_PAGE_DOWN:
448             case KeyEvent.KEYCODE_MOVE_END:
449                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
450                 break;
451             case KeyEvent.KEYCODE_DPAD_UP:
452             case KeyEvent.KEYCODE_PAGE_UP:
453             case KeyEvent.KEYCODE_MOVE_HOME:
454                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
455                 break;
456             default:
457                 break;
458         }
459     }
460 }
461