FocusHelper.java revision 18bfaafd3da45dce7b5f73eaa1665f228353338c
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
17package com.android.launcher3;
18
19import android.content.res.Configuration;
20import android.util.Log;
21import android.view.KeyEvent;
22import android.view.SoundEffectConstants;
23import android.view.View;
24import android.view.ViewGroup;
25
26import com.android.launcher3.FocusHelper.PagedViewKeyListener;
27import com.android.launcher3.util.FocusLogic;
28
29/**
30 * A keyboard listener we set on all the workspace icons.
31 */
32class IconKeyEventListener implements View.OnKeyListener {
33    @Override
34    public boolean onKey(View v, int keyCode, KeyEvent event) {
35        return FocusHelper.handleIconKeyEvent(v, keyCode, event);
36    }
37}
38
39/**
40 * A keyboard listener we set on all the workspace icons.
41 */
42class FolderKeyEventListener implements View.OnKeyListener {
43    @Override
44    public boolean onKey(View v, int keyCode, KeyEvent event) {
45        return FocusHelper.handleFolderKeyEvent(v, keyCode, event);
46    }
47}
48
49/**
50 * A keyboard listener we set on all the hotseat buttons.
51 */
52class HotseatIconKeyEventListener implements View.OnKeyListener {
53    @Override
54    public boolean onKey(View v, int keyCode, KeyEvent event) {
55        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
56    }
57}
58
59public class FocusHelper {
60
61    private static final String TAG = "FocusHelper";
62    private static final boolean DEBUG = false;
63
64    //
65    // Key code handling methods.
66    //
67
68    /**
69     * A keyboard listener for scrollable folders
70     */
71    public static class PagedFolderKeyEventListener extends PagedViewKeyListener {
72
73        private final Folder mFolder;
74
75        public PagedFolderKeyEventListener(Folder folder) {
76            mFolder = folder;
77        }
78
79        @Override
80        public void handleNoopKey(int keyCode, View v) {
81            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
82                mFolder.mFolderName.requestFocus();
83                playSoundEffect(keyCode, v);
84            }
85        }
86    }
87
88    /**
89     * Handles key events in the all apps screen.
90     */
91    public static class PagedViewKeyListener implements View.OnKeyListener {
92
93    @Override
94    public boolean onKey(View v, int keyCode, KeyEvent e) {
95        boolean consume = FocusLogic.shouldConsume(keyCode);
96        if (e.getAction() == KeyEvent.ACTION_UP) {
97            return consume;
98        }
99        if (DEBUG) {
100            Log.v(TAG, String.format("Handle ALL APPS keyevent=[%s].",
101                    KeyEvent.keyCodeToString(keyCode)));
102        }
103
104        // Initialize variables.
105        ViewGroup parentLayout;
106        ViewGroup itemContainer;
107        int countX;
108        int countY;
109        if (v.getParent() instanceof ShortcutAndWidgetContainer) {
110            itemContainer = (ViewGroup) v.getParent();
111            parentLayout = (ViewGroup) itemContainer.getParent();
112            countX = ((CellLayout) parentLayout).getCountX();
113            countY = ((CellLayout) parentLayout).getCountY();
114        } else {
115            if (LauncherAppState.isDogfoodBuild()) {
116                throw new IllegalStateException("Parent of the focused item is not supported.");
117            } else {
118                return false;
119            }
120        }
121
122        final int iconIndex = itemContainer.indexOfChild(v);
123        final PagedView container = (PagedView) parentLayout.getParent();
124        final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout));
125        final int pageCount = container.getChildCount();
126        ViewGroup newParent = null;
127        View child = null;
128        // TODO(hyunyoungs): this matrix is not applicable on the last page.
129        int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true);
130
131        // Process focus.
132        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
133                iconIndex, pageIndex, pageCount);
134        if (newIconIndex == FocusLogic.NOOP) {
135            handleNoopKey(keyCode, v);
136            return consume;
137        }
138        switch (newIconIndex) {
139            case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
140                newParent = getAppsCustomizePage(container, pageIndex -1);
141                if (newParent != null) {
142                    int row = FocusLogic.findRow(matrix, iconIndex);
143                    container.snapToPage(pageIndex - 1);
144                    // no need to create a new matrix.
145                    child = newParent.getChildAt(matrix[countX-1][row]);
146                }
147                break;
148            case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
149                newParent = getAppsCustomizePage(container, pageIndex - 1);
150                if (newParent != null) {
151                    container.snapToPage(pageIndex - 1);
152                    child = newParent.getChildAt(0);
153                }
154                break;
155            case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
156                newParent = getAppsCustomizePage(container, pageIndex - 1);
157                if (newParent != null) {
158                    container.snapToPage(pageIndex - 1);
159                    child = newParent.getChildAt(newParent.getChildCount() - 1);
160                }
161                break;
162            case FocusLogic.NEXT_PAGE_FIRST_ITEM:
163                newParent = getAppsCustomizePage(container, pageIndex + 1);
164                if (newParent != null) {
165                    container.snapToPage(pageIndex + 1);
166                    child = newParent.getChildAt(0);
167                }
168                break;
169            case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
170                newParent = getAppsCustomizePage(container, pageIndex + 1);
171                if (newParent != null) {
172                    container.snapToPage(pageIndex + 1);
173                    int row = FocusLogic.findRow(matrix, iconIndex);
174                    child = newParent.getChildAt(matrix[0][row]);
175                }
176                break;
177            case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
178                child = container.getChildAt(0);
179                break;
180            case FocusLogic.CURRENT_PAGE_LAST_ITEM:
181                child = itemContainer.getChildAt(itemContainer.getChildCount() - 1);
182                break;
183            default: // Go to some item on the current page.
184                child = itemContainer.getChildAt(newIconIndex);
185                break;
186        }
187        if (child != null) {
188            child.requestFocus();
189            playSoundEffect(keyCode, v);
190        } else {
191            handleNoopKey(keyCode, v);
192        }
193        return consume;
194    }
195
196    public void handleNoopKey(int keyCode, View v) { }
197    }
198
199    /**
200     * Handles key events in the workspace hot seat (bottom of the screen).
201     * <p>Currently we don't special case for the phone UI in different orientations, even though
202     * the hotseat is on the side in landscape mode. This is to ensure that accessibility
203     * consistency is maintained across rotations.
204     */
205    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
206        boolean consume = FocusLogic.shouldConsume(keyCode);
207        if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
208            return consume;
209        }
210
211        LauncherAppState app = LauncherAppState.getInstance();
212        DeviceProfile profile = app.getDynamicGrid().getDeviceProfile();
213        if (DEBUG) {
214            Log.v(TAG, String.format(
215                    "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
216                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
217        }
218
219        // Initialize the variables.
220        final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
221        final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
222        Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
223
224        Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
225        int pageIndex = workspace.getCurrentPage();
226        int pageCount = workspace.getChildCount();
227        int countX = -1;
228        int countY = -1;
229        int iconIndex = findIndexOfView(hotseatParent, v);
230        int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
231                .getChildAt(iconIndex).getLayoutParams()).cellX;
232
233        final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
234        final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
235
236        ViewGroup parent = null;
237        int[][] matrix = null;
238
239        if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
240                !profile.isVerticalBarLayout()) {
241            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
242                    true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
243                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
244            iconIndex += iconParent.getChildCount();
245            countX = iconLayout.getCountX();
246            countY = iconLayout.getCountY() + hotseatLayout.getCountY();
247            parent = iconParent;
248        } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
249                profile.isVerticalBarLayout()) {
250            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
251                    false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
252                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
253            iconIndex += iconParent.getChildCount();
254            countX = iconLayout.getCountX() + hotseatLayout.getCountX();
255            countY = iconLayout.getCountY();
256            parent = iconParent;
257        } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
258                profile.isVerticalBarLayout()) {
259            keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
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            countX = hotseatLayout.getCountX();
265            countY = hotseatLayout.getCountY();
266            parent = hotseatParent;
267        }
268
269        // Process the focus.
270        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
271                iconIndex, pageIndex, pageCount);
272
273        View newIcon = null;
274        if (newIconIndex == 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        }
281        if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
282            newIconIndex -= iconParent.getChildCount();
283        }
284        if (parent != null) {
285            if (newIcon == null && newIconIndex >=0) {
286                newIcon = parent.getChildAt(newIconIndex);
287            }
288            if (newIcon != null) {
289                newIcon.requestFocus();
290                playSoundEffect(keyCode, v);
291            }
292        }
293        return consume;
294    }
295
296    /**
297     * Handles key events in a workspace containing icons.
298     */
299    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
300        boolean consume = FocusLogic.shouldConsume(keyCode);
301        if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
302            return consume;
303        }
304
305        LauncherAppState app = LauncherAppState.getInstance();
306        DeviceProfile profile = app.getDynamicGrid().getDeviceProfile();
307
308        if (DEBUG) {
309            Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
310                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
311        }
312
313        // Initialize the variables.
314        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
315        CellLayout iconLayout = (CellLayout) parent.getParent();
316        final Workspace workspace = (Workspace) iconLayout.getParent();
317        final ViewGroup launcher = (ViewGroup) workspace.getParent();
318        final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
319        final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat);
320        int pageIndex = workspace.indexOfChild(iconLayout);
321        int pageCount = workspace.getChildCount();
322        int countX = iconLayout.getCountX();
323        int countY = iconLayout.getCountY();
324        final int iconIndex = findIndexOfView(parent, v);
325
326        CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
327        ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
328        int[][] matrix;
329
330        // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
331        // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
332        // with the hotseat.
333        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
334            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
335                    hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
336            countY = countY + 1;
337        } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
338                profile.isVerticalBarLayout()) {
339            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
340                    hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
341            countX = countX + 1;
342        } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
343            workspace.removeWorkspaceItem(v);
344            return consume;
345        } else {
346            matrix = FocusLogic.createSparseMatrix(iconLayout);
347        }
348
349        // Process the focus.
350        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
351                iconIndex, pageIndex, pageCount);
352        View newIcon = null;
353        switch (newIconIndex) {
354            case FocusLogic.NOOP:
355                if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
356                    newIcon = tabs;
357                }
358                break;
359            case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
360                int row = FocusLogic.findRow(matrix, iconIndex);
361                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
362                if (parent != null) {
363                    iconLayout = (CellLayout) parent.getParent();
364                    matrix = FocusLogic.createSparseMatrix(iconLayout,
365                        iconLayout.getCountX(), row);
366                    newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
367                        FocusLogic.PIVOT, pageIndex - 1, pageCount);
368                    newIcon = parent.getChildAt(newIconIndex);
369                }
370                break;
371            case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
372                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
373                newIcon = parent.getChildAt(0);
374                workspace.snapToPage(pageIndex - 1);
375                break;
376            case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
377                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
378                newIcon = parent.getChildAt(parent.getChildCount() - 1);
379                workspace.snapToPage(pageIndex - 1);
380                break;
381            case FocusLogic.NEXT_PAGE_FIRST_ITEM:
382                parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
383                newIcon = parent.getChildAt(0);
384                workspace.snapToPage(pageIndex + 1);
385                break;
386            case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
387                row = FocusLogic.findRow(matrix, iconIndex);
388                parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
389                if (parent != null) {
390                    iconLayout = (CellLayout) parent.getParent();
391                    matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
392                    newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
393                        FocusLogic.PIVOT, pageIndex, pageCount);
394                    newIcon = parent.getChildAt(newIconIndex);
395                }
396                break;
397            case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
398                newIcon = parent.getChildAt(0);
399                break;
400            case FocusLogic.CURRENT_PAGE_LAST_ITEM:
401                newIcon = parent.getChildAt(parent.getChildCount() - 1);
402                break;
403            default:
404                // current page, some item.
405                if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
406                    newIcon = parent.getChildAt(newIconIndex);
407                } else if (parent.getChildCount() <= newIconIndex &&
408                        newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
409                    newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
410                }
411                break;
412        }
413        if (newIcon != null) {
414            newIcon.requestFocus();
415            playSoundEffect(keyCode, v);
416        }
417        return consume;
418    }
419
420    /**
421     * Handles key events for items in a Folder.
422     */
423    static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
424        boolean consume = FocusLogic.shouldConsume(keyCode);
425        if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
426            return consume;
427        }
428        if (DEBUG) {
429            Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].",
430                    KeyEvent.keyCodeToString(keyCode)));
431        }
432
433        // Initialize the variables.
434        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
435        final CellLayout layout = (CellLayout) parent.getParent();
436        final Folder folder = (Folder) layout.getParent().getParent();
437        View title = folder.mFolderName;
438        Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
439        final int countX = layout.getCountX();
440        final int countY = layout.getCountY();
441        final int iconIndex = findIndexOfView(parent, v);
442        int pageIndex = workspace.indexOfChild(layout);
443        int pageCount = workspace.getChildCount();
444        int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order */);
445
446        // Process the focus.
447        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex,
448                pageIndex, pageCount);
449        View newIcon = null;
450        switch (newIconIndex) {
451            case FocusLogic.NOOP:
452                if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
453                    newIcon = title;
454                }
455                break;
456            case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
457            case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
458            case FocusLogic.NEXT_PAGE_FIRST_ITEM:
459            case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
460            case FocusLogic.CURRENT_PAGE_LAST_ITEM:
461                if (DEBUG) {
462                    Log.v(TAG, "Page advance handling not supported on folder icons.");
463                }
464                break;
465            default: // current page some item.
466                newIcon = parent.getChildAt(newIconIndex);
467                break;
468        }
469        if (newIcon != null) {
470            newIcon.requestFocus();
471            playSoundEffect(keyCode, v);
472        }
473        return consume;
474    }
475
476    //
477    // Helper methods.
478    //
479
480    /**
481     * Returns the Viewgroup containing page contents for the page at the index specified.
482     */
483    private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
484        ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
485        if (page instanceof CellLayout) {
486            // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
487            page = ((CellLayout) page).getShortcutsAndWidgets();
488        }
489        return page;
490    }
491
492    /**
493     * Private helper method to get the CellLayoutChildren given a CellLayout index.
494     */
495    private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
496            ViewGroup container, int i) {
497        CellLayout parent = (CellLayout) container.getChildAt(i);
498        return parent.getShortcutsAndWidgets();
499    }
500
501    private static int findIndexOfView(ViewGroup parent, View v) {
502        for (int i = 0; i < parent.getChildCount(); i++) {
503            if (v != null && v.equals(parent.getChildAt(i))) {
504                return i;
505            }
506        }
507        return -1;
508    }
509
510    /**
511     * Helper method to be used for playing sound effects.
512     */
513    private 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