1/*
2 * Copyright (C) 2011 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.view.ViewParent;
26import android.widget.ScrollView;
27
28import java.util.ArrayList;
29import java.util.Collections;
30import java.util.Comparator;
31
32/**
33 * A keyboard listener we set on all the workspace icons.
34 */
35class IconKeyEventListener implements View.OnKeyListener {
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 workspace icons.
43 */
44class FolderKeyEventListener implements View.OnKeyListener {
45    public boolean onKey(View v, int keyCode, KeyEvent event) {
46        return FocusHelper.handleFolderKeyEvent(v, keyCode, event);
47    }
48}
49
50/**
51 * A keyboard listener we set on all the hotseat buttons.
52 */
53class HotseatIconKeyEventListener implements View.OnKeyListener {
54    public boolean onKey(View v, int keyCode, KeyEvent event) {
55        final Configuration configuration = v.getResources().getConfiguration();
56        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation);
57    }
58}
59
60public class FocusHelper {
61    /**
62     * Private helper to get the parent TabHost in the view hiearchy.
63     */
64    private static AppsCustomizeTabHost findTabHostParent(View v) {
65        ViewParent p = v.getParent();
66        while (p != null && !(p instanceof AppsCustomizeTabHost)) {
67            p = p.getParent();
68        }
69        return (AppsCustomizeTabHost) p;
70    }
71
72    /**
73     * Returns the Viewgroup containing page contents for the page at the index specified.
74     */
75    private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
76        ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
77        if (page instanceof CellLayout) {
78            // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
79            page = ((CellLayout) page).getShortcutsAndWidgets();
80        }
81        return page;
82    }
83
84    /**
85     * Handles key events in a PageViewExtendedLayout containing PagedViewWidgets.
86     */
87    static boolean handlePagedViewGridLayoutWidgetKeyEvent(PagedViewWidget w, int keyCode,
88            KeyEvent e) {
89
90        final PagedViewGridLayout parent = (PagedViewGridLayout) w.getParent();
91        final PagedView container = (PagedView) parent.getParent();
92        final int widgetIndex = parent.indexOfChild(w);
93        final int widgetCount = parent.getChildCount();
94        final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parent));
95        final int pageCount = container.getChildCount();
96        final int cellCountX = parent.getCellCountX();
97        final int cellCountY = parent.getCellCountY();
98        final int x = widgetIndex % cellCountX;
99        final int y = widgetIndex / cellCountX;
100
101        final int action = e.getAction();
102        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
103        ViewGroup newParent = null;
104        // Now that we load items in the bg asynchronously, we can't just focus
105        // child siblings willy-nilly
106        View child = null;
107        boolean wasHandled = false;
108        switch (keyCode) {
109            case KeyEvent.KEYCODE_DPAD_LEFT:
110                if (handleKeyEvent) {
111                    // Select the previous widget or the last widget on the previous page
112                    if (widgetIndex > 0) {
113                        parent.getChildAt(widgetIndex - 1).requestFocus();
114                    } else {
115                        if (pageIndex > 0) {
116                            newParent = getAppsCustomizePage(container, pageIndex - 1);
117                            if (newParent != null) {
118                                child = newParent.getChildAt(newParent.getChildCount() - 1);
119                                if (child != null) child.requestFocus();
120                            }
121                        }
122                    }
123                }
124                wasHandled = true;
125                break;
126            case KeyEvent.KEYCODE_DPAD_RIGHT:
127                if (handleKeyEvent) {
128                    // Select the next widget or the first widget on the next page
129                    if (widgetIndex < (widgetCount - 1)) {
130                        parent.getChildAt(widgetIndex + 1).requestFocus();
131                    } else {
132                        if (pageIndex < (pageCount - 1)) {
133                            newParent = getAppsCustomizePage(container, pageIndex + 1);
134                            if (newParent != null) {
135                                child = newParent.getChildAt(0);
136                                if (child != null) child.requestFocus();
137                            }
138                        }
139                    }
140                }
141                wasHandled = true;
142                break;
143            case KeyEvent.KEYCODE_DPAD_UP:
144                if (handleKeyEvent) {
145                    // Select the closest icon in the previous row, otherwise select the tab bar
146                    if (y > 0) {
147                        int newWidgetIndex = ((y - 1) * cellCountX) + x;
148                        child = parent.getChildAt(newWidgetIndex);
149                        if (child != null) child.requestFocus();
150                    }
151                }
152                wasHandled = true;
153                break;
154            case KeyEvent.KEYCODE_DPAD_DOWN:
155                if (handleKeyEvent) {
156                    // Select the closest icon in the previous row, otherwise do nothing
157                    if (y < (cellCountY - 1)) {
158                        int newWidgetIndex = Math.min(widgetCount - 1, ((y + 1) * cellCountX) + x);
159                        child = parent.getChildAt(newWidgetIndex);
160                        if (child != null) child.requestFocus();
161                    }
162                }
163                wasHandled = true;
164                break;
165            case KeyEvent.KEYCODE_ENTER:
166            case KeyEvent.KEYCODE_DPAD_CENTER:
167                if (handleKeyEvent) {
168                    // Simulate a click on the widget
169                    View.OnClickListener clickListener = (View.OnClickListener) container;
170                    clickListener.onClick(w);
171                }
172                wasHandled = true;
173                break;
174            case KeyEvent.KEYCODE_PAGE_UP:
175                if (handleKeyEvent) {
176                    // Select the first item on the previous page, or the first item on this page
177                    // if there is no previous page
178                    if (pageIndex > 0) {
179                        newParent = getAppsCustomizePage(container, pageIndex - 1);
180                        if (newParent != null) {
181                            child = newParent.getChildAt(0);
182                        }
183                    } else {
184                        child = parent.getChildAt(0);
185                    }
186                    if (child != null) child.requestFocus();
187                }
188                wasHandled = true;
189                break;
190            case KeyEvent.KEYCODE_PAGE_DOWN:
191                if (handleKeyEvent) {
192                    // Select the first item on the next page, or the last item on this page
193                    // if there is no next page
194                    if (pageIndex < (pageCount - 1)) {
195                        newParent = getAppsCustomizePage(container, pageIndex + 1);
196                        if (newParent != null) {
197                            child = newParent.getChildAt(0);
198                        }
199                    } else {
200                        child = parent.getChildAt(widgetCount - 1);
201                    }
202                    if (child != null) child.requestFocus();
203                }
204                wasHandled = true;
205                break;
206            case KeyEvent.KEYCODE_MOVE_HOME:
207                if (handleKeyEvent) {
208                    // Select the first item on this page
209                    child = parent.getChildAt(0);
210                    if (child != null) child.requestFocus();
211                }
212                wasHandled = true;
213                break;
214            case KeyEvent.KEYCODE_MOVE_END:
215                if (handleKeyEvent) {
216                    // Select the last item on this page
217                    parent.getChildAt(widgetCount - 1).requestFocus();
218                }
219                wasHandled = true;
220                break;
221            default: break;
222        }
223        return wasHandled;
224    }
225
226    /**
227     * Handles key events in a PageViewCellLayout containing PagedViewIcons.
228     */
229    static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
230        ViewGroup parentLayout;
231        ViewGroup itemContainer;
232        int countX;
233        int countY;
234        if (v.getParent() instanceof ShortcutAndWidgetContainer) {
235            itemContainer = (ViewGroup) v.getParent();
236            parentLayout = (ViewGroup) itemContainer.getParent();
237            countX = ((CellLayout) parentLayout).getCountX();
238            countY = ((CellLayout) parentLayout).getCountY();
239        } else {
240            itemContainer = parentLayout = (ViewGroup) v.getParent();
241            countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
242            countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
243        }
244
245        // Note we have an extra parent because of the
246        // PagedViewCellLayout/PagedViewCellLayoutChildren relationship
247        final PagedView container = (PagedView) parentLayout.getParent();
248        final int iconIndex = itemContainer.indexOfChild(v);
249        final int itemCount = itemContainer.getChildCount();
250        final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout));
251        final int pageCount = container.getChildCount();
252
253        final int x = iconIndex % countX;
254        final int y = iconIndex / countX;
255
256        final int action = e.getAction();
257        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
258        ViewGroup newParent = null;
259        // Side pages do not always load synchronously, so check before focusing child siblings
260        // willy-nilly
261        View child = null;
262        boolean wasHandled = false;
263        switch (keyCode) {
264            case KeyEvent.KEYCODE_DPAD_LEFT:
265                if (handleKeyEvent) {
266                    // Select the previous icon or the last icon on the previous page
267                    if (iconIndex > 0) {
268                        itemContainer.getChildAt(iconIndex - 1).requestFocus();
269                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
270                    } else {
271                        if (pageIndex > 0) {
272                            newParent = getAppsCustomizePage(container, pageIndex - 1);
273                            if (newParent != null) {
274                                container.snapToPage(pageIndex - 1);
275                                child = newParent.getChildAt(newParent.getChildCount() - 1);
276                                if (child != null) {
277                                    child.requestFocus();
278                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
279                                }
280                            }
281                        }
282                    }
283                }
284                wasHandled = true;
285                break;
286            case KeyEvent.KEYCODE_DPAD_RIGHT:
287                if (handleKeyEvent) {
288                    // Select the next icon or the first icon on the next page
289                    if (iconIndex < (itemCount - 1)) {
290                        itemContainer.getChildAt(iconIndex + 1).requestFocus();
291                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
292                    } else {
293                        if (pageIndex < (pageCount - 1)) {
294                            newParent = getAppsCustomizePage(container, pageIndex + 1);
295                            if (newParent != null) {
296                                container.snapToPage(pageIndex + 1);
297                                child = newParent.getChildAt(0);
298                                if (child != null) {
299                                    child.requestFocus();
300                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
301                                }
302                            }
303                        }
304                    }
305                }
306                wasHandled = true;
307                break;
308            case KeyEvent.KEYCODE_DPAD_UP:
309                if (handleKeyEvent) {
310                    // Select the closest icon in the previous row, otherwise select the tab bar
311                    if (y > 0) {
312                        int newiconIndex = ((y - 1) * countX) + x;
313                        itemContainer.getChildAt(newiconIndex).requestFocus();
314                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
315                    }
316                }
317                wasHandled = true;
318                break;
319            case KeyEvent.KEYCODE_DPAD_DOWN:
320                if (handleKeyEvent) {
321                    // Select the closest icon in the next row, otherwise do nothing
322                    if (y < (countY - 1)) {
323                        int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x);
324                        int newIconY = newiconIndex / countX;
325                        if (newIconY != y) {
326                            itemContainer.getChildAt(newiconIndex).requestFocus();
327                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
328                        }
329                    }
330                }
331                wasHandled = true;
332                break;
333            case KeyEvent.KEYCODE_PAGE_UP:
334                if (handleKeyEvent) {
335                    // Select the first icon on the previous page, or the first icon on this page
336                    // if there is no previous page
337                    if (pageIndex > 0) {
338                        newParent = getAppsCustomizePage(container, pageIndex - 1);
339                        if (newParent != null) {
340                            container.snapToPage(pageIndex - 1);
341                            child = newParent.getChildAt(0);
342                            if (child != null) {
343                                child.requestFocus();
344                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
345                            }
346                        }
347                    } else {
348                        itemContainer.getChildAt(0).requestFocus();
349                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
350                    }
351                }
352                wasHandled = true;
353                break;
354            case KeyEvent.KEYCODE_PAGE_DOWN:
355                if (handleKeyEvent) {
356                    // Select the first icon on the next page, or the last icon on this page
357                    // if there is no next page
358                    if (pageIndex < (pageCount - 1)) {
359                        newParent = getAppsCustomizePage(container, pageIndex + 1);
360                        if (newParent != null) {
361                            container.snapToPage(pageIndex + 1);
362                            child = newParent.getChildAt(0);
363                            if (child != null) {
364                                child.requestFocus();
365                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
366                            }
367                        }
368                    } else {
369                        itemContainer.getChildAt(itemCount - 1).requestFocus();
370                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
371                    }
372                }
373                wasHandled = true;
374                break;
375            case KeyEvent.KEYCODE_MOVE_HOME:
376                if (handleKeyEvent) {
377                    // Select the first icon on this page
378                    itemContainer.getChildAt(0).requestFocus();
379                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
380                }
381                wasHandled = true;
382                break;
383            case KeyEvent.KEYCODE_MOVE_END:
384                if (handleKeyEvent) {
385                    // Select the last icon on this page
386                    itemContainer.getChildAt(itemCount - 1).requestFocus();
387                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
388                }
389                wasHandled = true;
390                break;
391            default: break;
392        }
393        return wasHandled;
394    }
395
396    /**
397     * Handles key events in the tab widget.
398     */
399    static boolean handleTabKeyEvent(AccessibleTabView v, int keyCode, KeyEvent e) {
400        if (!LauncherAppState.getInstance().isScreenLarge()) return false;
401
402        final FocusOnlyTabWidget parent = (FocusOnlyTabWidget) v.getParent();
403        final AppsCustomizeTabHost tabHost = findTabHostParent(parent);
404        final ViewGroup contents = tabHost.getContent();
405        final int tabCount = parent.getTabCount();
406        final int tabIndex = parent.getChildTabIndex(v);
407
408        final int action = e.getAction();
409        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
410        boolean wasHandled = false;
411        switch (keyCode) {
412            case KeyEvent.KEYCODE_DPAD_LEFT:
413                if (handleKeyEvent) {
414                    // Select the previous tab
415                    if (tabIndex > 0) {
416                        parent.getChildTabViewAt(tabIndex - 1).requestFocus();
417                    }
418                }
419                wasHandled = true;
420                break;
421            case KeyEvent.KEYCODE_DPAD_RIGHT:
422                if (handleKeyEvent) {
423                    // Select the next tab, or if the last tab has a focus right id, select that
424                    if (tabIndex < (tabCount - 1)) {
425                        parent.getChildTabViewAt(tabIndex + 1).requestFocus();
426                    } else {
427                        if (v.getNextFocusRightId() != View.NO_ID) {
428                            tabHost.findViewById(v.getNextFocusRightId()).requestFocus();
429                        }
430                    }
431                }
432                wasHandled = true;
433                break;
434            case KeyEvent.KEYCODE_DPAD_UP:
435                // Do nothing
436                wasHandled = true;
437                break;
438            case KeyEvent.KEYCODE_DPAD_DOWN:
439                if (handleKeyEvent) {
440                    // Select the content view
441                    contents.requestFocus();
442                }
443                wasHandled = true;
444                break;
445            default: break;
446        }
447        return wasHandled;
448    }
449
450    /**
451     * Handles key events in the workspace hotseat (bottom of the screen).
452     */
453    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) {
454        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
455        final CellLayout layout = (CellLayout) parent.getParent();
456
457        // NOTE: currently we don't special case for the phone UI in different
458        // orientations, even though the hotseat is on the side in landscape mode. This
459        // is to ensure that accessibility consistency is maintained across rotations.
460        final int action = e.getAction();
461        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
462        boolean wasHandled = false;
463        switch (keyCode) {
464            case KeyEvent.KEYCODE_DPAD_LEFT:
465                if (handleKeyEvent) {
466                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
467                    int myIndex = views.indexOf(v);
468                    // Select the previous button, otherwise do nothing
469                    if (myIndex > 0) {
470                        views.get(myIndex - 1).requestFocus();
471                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
472                    }
473                }
474                wasHandled = true;
475                break;
476            case KeyEvent.KEYCODE_DPAD_RIGHT:
477                if (handleKeyEvent) {
478                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
479                    int myIndex = views.indexOf(v);
480                    // Select the next button, otherwise do nothing
481                    if (myIndex < views.size() - 1) {
482                        views.get(myIndex + 1).requestFocus();
483                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
484                    }
485                }
486                wasHandled = true;
487                break;
488            case KeyEvent.KEYCODE_DPAD_UP:
489                if (handleKeyEvent) {
490                    final Workspace workspace = (Workspace)
491                            v.getRootView().findViewById(R.id.workspace);
492                    if (workspace != null) {
493                        int pageIndex = workspace.getCurrentPage();
494                        CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex);
495                        ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets();
496                        final View newIcon = getIconInDirection(layout, children, -1, 1);
497                        // Select the first bubble text view in the current page of the workspace
498                        if (newIcon != null) {
499                            newIcon.requestFocus();
500                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
501                        } else {
502                            workspace.requestFocus();
503                        }
504                    }
505                }
506                wasHandled = true;
507                break;
508            case KeyEvent.KEYCODE_DPAD_DOWN:
509                // Do nothing
510                wasHandled = true;
511                break;
512            default: break;
513        }
514        return wasHandled;
515    }
516
517    /**
518     * Private helper method to get the CellLayoutChildren given a CellLayout index.
519     */
520    private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
521            ViewGroup container, int i) {
522        CellLayout parent = (CellLayout) container.getChildAt(i);
523        return parent.getShortcutsAndWidgets();
524    }
525
526    /**
527     * Private helper method to sort all the CellLayout children in order of their (x,y) spatially
528     * from top left to bottom right.
529     */
530    private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout,
531            ViewGroup parent) {
532        // First we order each the CellLayout children by their x,y coordinates
533        final int cellCountX = layout.getCountX();
534        final int count = parent.getChildCount();
535        ArrayList<View> views = new ArrayList<View>();
536        for (int j = 0; j < count; ++j) {
537            views.add(parent.getChildAt(j));
538        }
539        Collections.sort(views, new Comparator<View>() {
540            @Override
541            public int compare(View lhs, View rhs) {
542                CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams();
543                CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams();
544                int lvIndex = (llp.cellY * cellCountX) + llp.cellX;
545                int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX;
546                return lvIndex - rvIndex;
547            }
548        });
549        return views;
550    }
551    /**
552     * Private helper method to find the index of the next BubbleTextView or FolderIcon in the
553     * direction delta.
554     *
555     * @param delta either -1 or 1 depending on the direction we want to search
556     */
557    private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) {
558        // Then we find the next BubbleTextView offset by delta from i
559        final int count = views.size();
560        int newI = i + delta;
561        while (0 <= newI && newI < count) {
562            View newV = views.get(newI);
563            if (newV instanceof BubbleTextView || newV instanceof FolderIcon) {
564                return newV;
565            }
566            newI += delta;
567        }
568        return null;
569    }
570    private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i,
571            int delta) {
572        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
573        return findIndexOfIcon(views, i, delta);
574    }
575    private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v,
576            int delta) {
577        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
578        return findIndexOfIcon(views, views.indexOf(v), delta);
579    }
580    /**
581     * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction
582     * delta on the next line.
583     *
584     * @param delta either -1 or 1 depending on the line and direction we want to search
585     */
586    private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v,
587            int lineDelta) {
588        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
589        final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
590        final int cellCountY = layout.getCountY();
591        final int row = lp.cellY;
592        final int newRow = row + lineDelta;
593        if (0 <= newRow && newRow < cellCountY) {
594            float closestDistance = Float.MAX_VALUE;
595            int closestIndex = -1;
596            int index = views.indexOf(v);
597            int endIndex = (lineDelta < 0) ? -1 : views.size();
598            while (index != endIndex) {
599                View newV = views.get(index);
600                CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams();
601                boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row);
602                if (satisfiesRow &&
603                        (newV instanceof BubbleTextView || newV instanceof FolderIcon)) {
604                    float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) +
605                            Math.pow(tmpLp.cellY - lp.cellY, 2));
606                    if (tmpDistance < closestDistance) {
607                        closestIndex = index;
608                        closestDistance = tmpDistance;
609                    }
610                }
611                if (index <= endIndex) {
612                    ++index;
613                } else {
614                    --index;
615                }
616            }
617            if (closestIndex > -1) {
618                return views.get(closestIndex);
619            }
620        }
621        return null;
622    }
623
624    /**
625     * Handles key events in a Workspace containing.
626     */
627    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
628        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
629        final CellLayout layout = (CellLayout) parent.getParent();
630        final Workspace workspace = (Workspace) layout.getParent();
631        final ViewGroup launcher = (ViewGroup) workspace.getParent();
632        final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
633        final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat);
634        int pageIndex = workspace.indexOfChild(layout);
635        int pageCount = workspace.getChildCount();
636
637        final int action = e.getAction();
638        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
639        boolean wasHandled = false;
640        switch (keyCode) {
641            case KeyEvent.KEYCODE_DPAD_LEFT:
642                if (handleKeyEvent) {
643                    // Select the previous icon or the last icon on the previous page if possible
644                    View newIcon = getIconInDirection(layout, parent, v, -1);
645                    if (newIcon != null) {
646                        newIcon.requestFocus();
647                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
648                    } else {
649                        if (pageIndex > 0) {
650                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
651                            newIcon = getIconInDirection(layout, parent,
652                                    parent.getChildCount(), -1);
653                            if (newIcon != null) {
654                                newIcon.requestFocus();
655                            } else {
656                                // Snap to the previous page
657                                workspace.snapToPage(pageIndex - 1);
658                            }
659                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
660                        }
661                    }
662                }
663                wasHandled = true;
664                break;
665            case KeyEvent.KEYCODE_DPAD_RIGHT:
666                if (handleKeyEvent) {
667                    // Select the next icon or the first icon on the next page if possible
668                    View newIcon = getIconInDirection(layout, parent, v, 1);
669                    if (newIcon != null) {
670                        newIcon.requestFocus();
671                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
672                    } else {
673                        if (pageIndex < (pageCount - 1)) {
674                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
675                            newIcon = getIconInDirection(layout, parent, -1, 1);
676                            if (newIcon != null) {
677                                newIcon.requestFocus();
678                            } else {
679                                // Snap to the next page
680                                workspace.snapToPage(pageIndex + 1);
681                            }
682                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
683                        }
684                    }
685                }
686                wasHandled = true;
687                break;
688            case KeyEvent.KEYCODE_DPAD_UP:
689                if (handleKeyEvent) {
690                    // Select the closest icon in the previous line, otherwise select the tab bar
691                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
692                    if (newIcon != null) {
693                        newIcon.requestFocus();
694                        wasHandled = true;
695                    } else {
696                        tabs.requestFocus();
697                    }
698                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
699                }
700                break;
701            case KeyEvent.KEYCODE_DPAD_DOWN:
702                if (handleKeyEvent) {
703                    // Select the closest icon in the next line, otherwise select the button bar
704                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
705                    if (newIcon != null) {
706                        newIcon.requestFocus();
707                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
708                        wasHandled = true;
709                    } else if (hotseat != null) {
710                        hotseat.requestFocus();
711                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
712                    }
713                }
714                break;
715            case KeyEvent.KEYCODE_PAGE_UP:
716                if (handleKeyEvent) {
717                    // Select the first icon on the previous page or the first icon on this page
718                    // if there is no previous page
719                    if (pageIndex > 0) {
720                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
721                        View newIcon = getIconInDirection(layout, parent, -1, 1);
722                        if (newIcon != null) {
723                            newIcon.requestFocus();
724                        } else {
725                            // Snap to the previous page
726                            workspace.snapToPage(pageIndex - 1);
727                        }
728                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
729                    } else {
730                        View newIcon = getIconInDirection(layout, parent, -1, 1);
731                        if (newIcon != null) {
732                            newIcon.requestFocus();
733                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
734                        }
735                    }
736                }
737                wasHandled = true;
738                break;
739            case KeyEvent.KEYCODE_PAGE_DOWN:
740                if (handleKeyEvent) {
741                    // Select the first icon on the next page or the last icon on this page
742                    // if there is no previous page
743                    if (pageIndex < (pageCount - 1)) {
744                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
745                        View newIcon = getIconInDirection(layout, parent, -1, 1);
746                        if (newIcon != null) {
747                            newIcon.requestFocus();
748                        } else {
749                            // Snap to the next page
750                            workspace.snapToPage(pageIndex + 1);
751                        }
752                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
753                    } else {
754                        View newIcon = getIconInDirection(layout, parent,
755                                parent.getChildCount(), -1);
756                        if (newIcon != null) {
757                            newIcon.requestFocus();
758                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
759                        }
760                    }
761                }
762                wasHandled = true;
763                break;
764            case KeyEvent.KEYCODE_MOVE_HOME:
765                if (handleKeyEvent) {
766                    // Select the first icon on this page
767                    View newIcon = getIconInDirection(layout, parent, -1, 1);
768                    if (newIcon != null) {
769                        newIcon.requestFocus();
770                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
771                    }
772                }
773                wasHandled = true;
774                break;
775            case KeyEvent.KEYCODE_MOVE_END:
776                if (handleKeyEvent) {
777                    // Select the last icon on this page
778                    View newIcon = getIconInDirection(layout, parent,
779                            parent.getChildCount(), -1);
780                    if (newIcon != null) {
781                        newIcon.requestFocus();
782                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
783                    }
784                }
785                wasHandled = true;
786                break;
787            default: break;
788        }
789        return wasHandled;
790    }
791
792    /**
793     * Handles key events for items in a Folder.
794     */
795    static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
796        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
797        final CellLayout layout = (CellLayout) parent.getParent();
798        final ScrollView scrollView = (ScrollView) layout.getParent();
799        final Folder folder = (Folder) scrollView.getParent();
800        View title = folder.mFolderName;
801
802        final int action = e.getAction();
803        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
804        boolean wasHandled = false;
805        switch (keyCode) {
806            case KeyEvent.KEYCODE_DPAD_LEFT:
807                if (handleKeyEvent) {
808                    // Select the previous icon
809                    View newIcon = getIconInDirection(layout, parent, v, -1);
810                    if (newIcon != null) {
811                        newIcon.requestFocus();
812                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
813                    }
814                }
815                wasHandled = true;
816                break;
817            case KeyEvent.KEYCODE_DPAD_RIGHT:
818                if (handleKeyEvent) {
819                    // Select the next icon
820                    View newIcon = getIconInDirection(layout, parent, v, 1);
821                    if (newIcon != null) {
822                        newIcon.requestFocus();
823                    } else {
824                        title.requestFocus();
825                    }
826                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
827                }
828                wasHandled = true;
829                break;
830            case KeyEvent.KEYCODE_DPAD_UP:
831                if (handleKeyEvent) {
832                    // Select the closest icon in the previous line
833                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
834                    if (newIcon != null) {
835                        newIcon.requestFocus();
836                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
837                    }
838                }
839                wasHandled = true;
840                break;
841            case KeyEvent.KEYCODE_DPAD_DOWN:
842                if (handleKeyEvent) {
843                    // Select the closest icon in the next line
844                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
845                    if (newIcon != null) {
846                        newIcon.requestFocus();
847                    } else {
848                        title.requestFocus();
849                    }
850                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
851                }
852                wasHandled = true;
853                break;
854            case KeyEvent.KEYCODE_MOVE_HOME:
855                if (handleKeyEvent) {
856                    // Select the first icon on this page
857                    View newIcon = getIconInDirection(layout, parent, -1, 1);
858                    if (newIcon != null) {
859                        newIcon.requestFocus();
860                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
861                    }
862                }
863                wasHandled = true;
864                break;
865            case KeyEvent.KEYCODE_MOVE_END:
866                if (handleKeyEvent) {
867                    // Select the last icon on this page
868                    View newIcon = getIconInDirection(layout, parent,
869                            parent.getChildCount(), -1);
870                    if (newIcon != null) {
871                        newIcon.requestFocus();
872                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
873                    }
874                }
875                wasHandled = true;
876                break;
877            default: break;
878        }
879        return wasHandled;
880    }
881}
882