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.view.KeyEvent;
21import android.view.SoundEffectConstants;
22import android.view.View;
23import android.view.ViewGroup;
24import android.widget.ScrollView;
25
26import java.util.ArrayList;
27import java.util.Collections;
28import java.util.Comparator;
29
30/**
31 * A keyboard listener we set on all the workspace icons.
32 */
33class IconKeyEventListener implements View.OnKeyListener {
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    public boolean onKey(View v, int keyCode, KeyEvent event) {
44        return FocusHelper.handleFolderKeyEvent(v, keyCode, event);
45    }
46}
47
48/**
49 * A keyboard listener we set on all the hotseat buttons.
50 */
51class HotseatIconKeyEventListener implements View.OnKeyListener {
52    public boolean onKey(View v, int keyCode, KeyEvent event) {
53        final Configuration configuration = v.getResources().getConfiguration();
54        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation);
55    }
56}
57
58public class FocusHelper {
59
60    /**
61     * Returns the Viewgroup containing page contents for the page at the index specified.
62     */
63    private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
64        ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
65        if (page instanceof CellLayout) {
66            // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
67            page = ((CellLayout) page).getShortcutsAndWidgets();
68        }
69        return page;
70    }
71
72    /**
73     * Handles key events in a PageViewCellLayout containing PagedViewIcons.
74     */
75    static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
76        ViewGroup parentLayout;
77        ViewGroup itemContainer;
78        int countX;
79        int countY;
80        if (v.getParent() instanceof ShortcutAndWidgetContainer) {
81            itemContainer = (ViewGroup) v.getParent();
82            parentLayout = (ViewGroup) itemContainer.getParent();
83            countX = ((CellLayout) parentLayout).getCountX();
84            countY = ((CellLayout) parentLayout).getCountY();
85        } else {
86            itemContainer = parentLayout = (ViewGroup) v.getParent();
87            countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
88            countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
89        }
90
91        // Note we have an extra parent because of the
92        // PagedViewCellLayout/PagedViewCellLayoutChildren relationship
93        final PagedView container = (PagedView) parentLayout.getParent();
94        final int iconIndex = itemContainer.indexOfChild(v);
95        final int itemCount = itemContainer.getChildCount();
96        final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout));
97        final int pageCount = container.getChildCount();
98
99        final int x = iconIndex % countX;
100        final int y = iconIndex / countX;
101
102        final int action = e.getAction();
103        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
104        ViewGroup newParent = null;
105        // Side pages do not always load synchronously, so check before focusing child siblings
106        // willy-nilly
107        View child = null;
108        boolean wasHandled = false;
109        switch (keyCode) {
110            case KeyEvent.KEYCODE_DPAD_LEFT:
111                if (handleKeyEvent) {
112                    // Select the previous icon or the last icon on the previous page
113                    if (iconIndex > 0) {
114                        itemContainer.getChildAt(iconIndex - 1).requestFocus();
115                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
116                    } else {
117                        if (pageIndex > 0) {
118                            newParent = getAppsCustomizePage(container, pageIndex - 1);
119                            if (newParent != null) {
120                                container.snapToPage(pageIndex - 1);
121                                child = newParent.getChildAt(newParent.getChildCount() - 1);
122                                if (child != null) {
123                                    child.requestFocus();
124                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
125                                }
126                            }
127                        }
128                    }
129                }
130                wasHandled = true;
131                break;
132            case KeyEvent.KEYCODE_DPAD_RIGHT:
133                if (handleKeyEvent) {
134                    // Select the next icon or the first icon on the next page
135                    if (iconIndex < (itemCount - 1)) {
136                        itemContainer.getChildAt(iconIndex + 1).requestFocus();
137                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
138                    } else {
139                        if (pageIndex < (pageCount - 1)) {
140                            newParent = getAppsCustomizePage(container, pageIndex + 1);
141                            if (newParent != null) {
142                                container.snapToPage(pageIndex + 1);
143                                child = newParent.getChildAt(0);
144                                if (child != null) {
145                                    child.requestFocus();
146                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
147                                }
148                            }
149                        }
150                    }
151                }
152                wasHandled = true;
153                break;
154            case KeyEvent.KEYCODE_DPAD_UP:
155                if (handleKeyEvent) {
156                    // Select the closest icon in the previous row, otherwise select the tab bar
157                    if (y > 0) {
158                        int newiconIndex = ((y - 1) * countX) + x;
159                        itemContainer.getChildAt(newiconIndex).requestFocus();
160                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
161                    }
162                }
163                wasHandled = true;
164                break;
165            case KeyEvent.KEYCODE_DPAD_DOWN:
166                if (handleKeyEvent) {
167                    // Select the closest icon in the next row, otherwise do nothing
168                    if (y < (countY - 1)) {
169                        int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x);
170                        int newIconY = newiconIndex / countX;
171                        if (newIconY != y) {
172                            itemContainer.getChildAt(newiconIndex).requestFocus();
173                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
174                        }
175                    }
176                }
177                wasHandled = true;
178                break;
179            case KeyEvent.KEYCODE_PAGE_UP:
180                if (handleKeyEvent) {
181                    // Select the first icon on the previous page, or the first icon on this page
182                    // if there is no previous page
183                    if (pageIndex > 0) {
184                        newParent = getAppsCustomizePage(container, pageIndex - 1);
185                        if (newParent != null) {
186                            container.snapToPage(pageIndex - 1);
187                            child = newParent.getChildAt(0);
188                            if (child != null) {
189                                child.requestFocus();
190                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
191                            }
192                        }
193                    } else {
194                        itemContainer.getChildAt(0).requestFocus();
195                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
196                    }
197                }
198                wasHandled = true;
199                break;
200            case KeyEvent.KEYCODE_PAGE_DOWN:
201                if (handleKeyEvent) {
202                    // Select the first icon on the next page, or the last icon on this page
203                    // if there is no next page
204                    if (pageIndex < (pageCount - 1)) {
205                        newParent = getAppsCustomizePage(container, pageIndex + 1);
206                        if (newParent != null) {
207                            container.snapToPage(pageIndex + 1);
208                            child = newParent.getChildAt(0);
209                            if (child != null) {
210                                child.requestFocus();
211                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
212                            }
213                        }
214                    } else {
215                        itemContainer.getChildAt(itemCount - 1).requestFocus();
216                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
217                    }
218                }
219                wasHandled = true;
220                break;
221            case KeyEvent.KEYCODE_MOVE_HOME:
222                if (handleKeyEvent) {
223                    // Select the first icon on this page
224                    itemContainer.getChildAt(0).requestFocus();
225                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
226                }
227                wasHandled = true;
228                break;
229            case KeyEvent.KEYCODE_MOVE_END:
230                if (handleKeyEvent) {
231                    // Select the last icon on this page
232                    itemContainer.getChildAt(itemCount - 1).requestFocus();
233                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
234                }
235                wasHandled = true;
236                break;
237            default: break;
238        }
239        return wasHandled;
240    }
241
242    /**
243     * Handles key events in the workspace hotseat (bottom of the screen).
244     */
245    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) {
246        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
247        final CellLayout layout = (CellLayout) parent.getParent();
248
249        // NOTE: currently we don't special case for the phone UI in different
250        // orientations, even though the hotseat is on the side in landscape mode. This
251        // is to ensure that accessibility consistency is maintained across rotations.
252        final int action = e.getAction();
253        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
254        boolean wasHandled = false;
255        switch (keyCode) {
256            case KeyEvent.KEYCODE_DPAD_LEFT:
257                if (handleKeyEvent) {
258                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
259                    int myIndex = views.indexOf(v);
260                    // Select the previous button, otherwise do nothing
261                    if (myIndex > 0) {
262                        views.get(myIndex - 1).requestFocus();
263                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
264                    }
265                }
266                wasHandled = true;
267                break;
268            case KeyEvent.KEYCODE_DPAD_RIGHT:
269                if (handleKeyEvent) {
270                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
271                    int myIndex = views.indexOf(v);
272                    // Select the next button, otherwise do nothing
273                    if (myIndex < views.size() - 1) {
274                        views.get(myIndex + 1).requestFocus();
275                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
276                    }
277                }
278                wasHandled = true;
279                break;
280            case KeyEvent.KEYCODE_DPAD_UP:
281                if (handleKeyEvent) {
282                    final Workspace workspace = (Workspace)
283                            v.getRootView().findViewById(R.id.workspace);
284                    if (workspace != null) {
285                        int pageIndex = workspace.getCurrentPage();
286                        CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex);
287                        ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets();
288                        final View newIcon = getIconInDirection(layout, children, -1, 1);
289                        // Select the first bubble text view in the current page of the workspace
290                        if (newIcon != null) {
291                            newIcon.requestFocus();
292                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
293                        } else {
294                            workspace.requestFocus();
295                        }
296                    }
297                }
298                wasHandled = true;
299                break;
300            case KeyEvent.KEYCODE_DPAD_DOWN:
301                // Do nothing
302                wasHandled = true;
303                break;
304            default: break;
305        }
306        return wasHandled;
307    }
308
309    /**
310     * Private helper method to get the CellLayoutChildren given a CellLayout index.
311     */
312    private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
313            ViewGroup container, int i) {
314        CellLayout parent = (CellLayout) container.getChildAt(i);
315        return parent.getShortcutsAndWidgets();
316    }
317
318    /**
319     * Private helper method to sort all the CellLayout children in order of their (x,y) spatially
320     * from top left to bottom right.
321     */
322    private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout,
323            ViewGroup parent) {
324        // First we order each the CellLayout children by their x,y coordinates
325        final int cellCountX = layout.getCountX();
326        final int count = parent.getChildCount();
327        ArrayList<View> views = new ArrayList<View>();
328        for (int j = 0; j < count; ++j) {
329            views.add(parent.getChildAt(j));
330        }
331        Collections.sort(views, new Comparator<View>() {
332            @Override
333            public int compare(View lhs, View rhs) {
334                CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams();
335                CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams();
336                int lvIndex = (llp.cellY * cellCountX) + llp.cellX;
337                int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX;
338                return lvIndex - rvIndex;
339            }
340        });
341        return views;
342    }
343    /**
344     * Private helper method to find the index of the next BubbleTextView or FolderIcon in the
345     * direction delta.
346     *
347     * @param delta either -1 or 1 depending on the direction we want to search
348     */
349    private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) {
350        // Then we find the next BubbleTextView offset by delta from i
351        final int count = views.size();
352        int newI = i + delta;
353        while (0 <= newI && newI < count) {
354            View newV = views.get(newI);
355            if (newV instanceof BubbleTextView || newV instanceof FolderIcon) {
356                return newV;
357            }
358            newI += delta;
359        }
360        return null;
361    }
362    private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i,
363            int delta) {
364        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
365        return findIndexOfIcon(views, i, delta);
366    }
367    private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v,
368            int delta) {
369        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
370        return findIndexOfIcon(views, views.indexOf(v), delta);
371    }
372    /**
373     * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction
374     * delta on the next line.
375     *
376     * @param delta either -1 or 1 depending on the line and direction we want to search
377     */
378    private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v,
379            int lineDelta) {
380        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
381        final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
382        final int cellCountY = layout.getCountY();
383        final int row = lp.cellY;
384        final int newRow = row + lineDelta;
385        if (0 <= newRow && newRow < cellCountY) {
386            float closestDistance = Float.MAX_VALUE;
387            int closestIndex = -1;
388            int index = views.indexOf(v);
389            int endIndex = (lineDelta < 0) ? -1 : views.size();
390            while (index != endIndex) {
391                View newV = views.get(index);
392                CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams();
393                boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row);
394                if (satisfiesRow &&
395                        (newV instanceof BubbleTextView || newV instanceof FolderIcon)) {
396                    float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) +
397                            Math.pow(tmpLp.cellY - lp.cellY, 2));
398                    if (tmpDistance < closestDistance) {
399                        closestIndex = index;
400                        closestDistance = tmpDistance;
401                    }
402                }
403                if (index <= endIndex) {
404                    ++index;
405                } else {
406                    --index;
407                }
408            }
409            if (closestIndex > -1) {
410                return views.get(closestIndex);
411            }
412        }
413        return null;
414    }
415
416    /**
417     * Handles key events in a Workspace containing.
418     */
419    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
420        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
421        final CellLayout layout = (CellLayout) parent.getParent();
422        final Workspace workspace = (Workspace) layout.getParent();
423        final ViewGroup launcher = (ViewGroup) workspace.getParent();
424        final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
425        final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat);
426        int pageIndex = workspace.indexOfChild(layout);
427        int pageCount = workspace.getChildCount();
428
429        final int action = e.getAction();
430        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
431        boolean wasHandled = false;
432        switch (keyCode) {
433            case KeyEvent.KEYCODE_DPAD_LEFT:
434                if (handleKeyEvent) {
435                    // Select the previous icon or the last icon on the previous page if possible
436                    View newIcon = getIconInDirection(layout, parent, v, -1);
437                    if (newIcon != null) {
438                        newIcon.requestFocus();
439                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
440                    } else {
441                        if (pageIndex > 0) {
442                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
443                            newIcon = getIconInDirection(layout, parent,
444                                    parent.getChildCount(), -1);
445                            if (newIcon != null) {
446                                newIcon.requestFocus();
447                            } else {
448                                // Snap to the previous page
449                                workspace.snapToPage(pageIndex - 1);
450                            }
451                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
452                        }
453                    }
454                }
455                wasHandled = true;
456                break;
457            case KeyEvent.KEYCODE_DPAD_RIGHT:
458                if (handleKeyEvent) {
459                    // Select the next icon or the first icon on the next page if possible
460                    View newIcon = getIconInDirection(layout, parent, v, 1);
461                    if (newIcon != null) {
462                        newIcon.requestFocus();
463                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
464                    } else {
465                        if (pageIndex < (pageCount - 1)) {
466                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
467                            newIcon = getIconInDirection(layout, parent, -1, 1);
468                            if (newIcon != null) {
469                                newIcon.requestFocus();
470                            } else {
471                                // Snap to the next page
472                                workspace.snapToPage(pageIndex + 1);
473                            }
474                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
475                        }
476                    }
477                }
478                wasHandled = true;
479                break;
480            case KeyEvent.KEYCODE_DPAD_UP:
481                if (handleKeyEvent) {
482                    // Select the closest icon in the previous line, otherwise select the tab bar
483                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
484                    if (newIcon != null) {
485                        newIcon.requestFocus();
486                        wasHandled = true;
487                    } else {
488                        tabs.requestFocus();
489                    }
490                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
491                }
492                break;
493            case KeyEvent.KEYCODE_DPAD_DOWN:
494                if (handleKeyEvent) {
495                    // Select the closest icon in the next line, otherwise select the button bar
496                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
497                    if (newIcon != null) {
498                        newIcon.requestFocus();
499                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
500                        wasHandled = true;
501                    } else if (hotseat != null) {
502                        hotseat.requestFocus();
503                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
504                    }
505                }
506                break;
507            case KeyEvent.KEYCODE_PAGE_UP:
508                if (handleKeyEvent) {
509                    // Select the first icon on the previous page or the first icon on this page
510                    // if there is no previous page
511                    if (pageIndex > 0) {
512                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
513                        View newIcon = getIconInDirection(layout, parent, -1, 1);
514                        if (newIcon != null) {
515                            newIcon.requestFocus();
516                        } else {
517                            // Snap to the previous page
518                            workspace.snapToPage(pageIndex - 1);
519                        }
520                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
521                    } else {
522                        View newIcon = getIconInDirection(layout, parent, -1, 1);
523                        if (newIcon != null) {
524                            newIcon.requestFocus();
525                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
526                        }
527                    }
528                }
529                wasHandled = true;
530                break;
531            case KeyEvent.KEYCODE_PAGE_DOWN:
532                if (handleKeyEvent) {
533                    // Select the first icon on the next page or the last icon on this page
534                    // if there is no previous page
535                    if (pageIndex < (pageCount - 1)) {
536                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
537                        View newIcon = getIconInDirection(layout, parent, -1, 1);
538                        if (newIcon != null) {
539                            newIcon.requestFocus();
540                        } else {
541                            // Snap to the next page
542                            workspace.snapToPage(pageIndex + 1);
543                        }
544                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
545                    } else {
546                        View newIcon = getIconInDirection(layout, parent,
547                                parent.getChildCount(), -1);
548                        if (newIcon != null) {
549                            newIcon.requestFocus();
550                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
551                        }
552                    }
553                }
554                wasHandled = true;
555                break;
556            case KeyEvent.KEYCODE_MOVE_HOME:
557                if (handleKeyEvent) {
558                    // Select the first icon on this page
559                    View newIcon = getIconInDirection(layout, parent, -1, 1);
560                    if (newIcon != null) {
561                        newIcon.requestFocus();
562                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
563                    }
564                }
565                wasHandled = true;
566                break;
567            case KeyEvent.KEYCODE_MOVE_END:
568                if (handleKeyEvent) {
569                    // Select the last icon on this page
570                    View newIcon = getIconInDirection(layout, parent,
571                            parent.getChildCount(), -1);
572                    if (newIcon != null) {
573                        newIcon.requestFocus();
574                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
575                    }
576                }
577                wasHandled = true;
578                break;
579            default: break;
580        }
581        return wasHandled;
582    }
583
584    /**
585     * Handles key events for items in a Folder.
586     */
587    static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
588        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
589        final CellLayout layout = (CellLayout) parent.getParent();
590        final ScrollView scrollView = (ScrollView) layout.getParent();
591        final Folder folder = (Folder) scrollView.getParent();
592        View title = folder.mFolderName;
593
594        final int action = e.getAction();
595        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
596        boolean wasHandled = false;
597        switch (keyCode) {
598            case KeyEvent.KEYCODE_DPAD_LEFT:
599                if (handleKeyEvent) {
600                    // Select the previous icon
601                    View newIcon = getIconInDirection(layout, parent, v, -1);
602                    if (newIcon != null) {
603                        newIcon.requestFocus();
604                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
605                    }
606                }
607                wasHandled = true;
608                break;
609            case KeyEvent.KEYCODE_DPAD_RIGHT:
610                if (handleKeyEvent) {
611                    // Select the next icon
612                    View newIcon = getIconInDirection(layout, parent, v, 1);
613                    if (newIcon != null) {
614                        newIcon.requestFocus();
615                    } else {
616                        title.requestFocus();
617                    }
618                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
619                }
620                wasHandled = true;
621                break;
622            case KeyEvent.KEYCODE_DPAD_UP:
623                if (handleKeyEvent) {
624                    // Select the closest icon in the previous line
625                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
626                    if (newIcon != null) {
627                        newIcon.requestFocus();
628                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
629                    }
630                }
631                wasHandled = true;
632                break;
633            case KeyEvent.KEYCODE_DPAD_DOWN:
634                if (handleKeyEvent) {
635                    // Select the closest icon in the next line
636                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
637                    if (newIcon != null) {
638                        newIcon.requestFocus();
639                    } else {
640                        title.requestFocus();
641                    }
642                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
643                }
644                wasHandled = true;
645                break;
646            case KeyEvent.KEYCODE_MOVE_HOME:
647                if (handleKeyEvent) {
648                    // Select the first icon on this page
649                    View newIcon = getIconInDirection(layout, parent, -1, 1);
650                    if (newIcon != null) {
651                        newIcon.requestFocus();
652                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
653                    }
654                }
655                wasHandled = true;
656                break;
657            case KeyEvent.KEYCODE_MOVE_END:
658                if (handleKeyEvent) {
659                    // Select the last icon on this page
660                    View newIcon = getIconInDirection(layout, parent,
661                            parent.getChildCount(), -1);
662                    if (newIcon != null) {
663                        newIcon.requestFocus();
664                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
665                    }
666                }
667                wasHandled = true;
668                break;
669            default: break;
670        }
671        return wasHandled;
672    }
673}
674