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