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