Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.launcher3;
     18 
     19 import android.util.Log;
     20 import android.view.KeyEvent;
     21 import android.view.SoundEffectConstants;
     22 import android.view.View;
     23 import android.view.ViewGroup;
     24 
     25 import com.android.launcher3.util.FocusLogic;
     26 import com.android.launcher3.util.Thunk;
     27 
     28 /**
     29  * A keyboard listener we set on all the workspace icons.
     30  */
     31 class IconKeyEventListener implements View.OnKeyListener {
     32     @Override
     33     public boolean onKey(View v, int keyCode, KeyEvent event) {
     34         return FocusHelper.handleIconKeyEvent(v, keyCode, event);
     35     }
     36 }
     37 
     38 /**
     39  * A keyboard listener we set on all the hotseat buttons.
     40  */
     41 class HotseatIconKeyEventListener implements View.OnKeyListener {
     42     @Override
     43     public boolean onKey(View v, int keyCode, KeyEvent event) {
     44         return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
     45     }
     46 }
     47 
     48 public class FocusHelper {
     49 
     50     private static final String TAG = "FocusHelper";
     51     private static final boolean DEBUG = false;
     52 
     53     /**
     54      * Handles key events in paged folder.
     55      */
     56     public static class PagedFolderKeyEventListener implements View.OnKeyListener {
     57 
     58         private final Folder mFolder;
     59 
     60         public PagedFolderKeyEventListener(Folder folder) {
     61             mFolder = folder;
     62         }
     63 
     64         @Override
     65         public boolean onKey(View v, int keyCode, KeyEvent e) {
     66             boolean consume = FocusLogic.shouldConsume(keyCode);
     67             if (e.getAction() == KeyEvent.ACTION_UP) {
     68                 return consume;
     69             }
     70             if (DEBUG) {
     71                 Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
     72                         KeyEvent.keyCodeToString(keyCode)));
     73             }
     74 
     75 
     76             if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
     77                 if (LauncherAppState.isDogfoodBuild()) {
     78                     throw new IllegalStateException("Parent of the focused item is not supported.");
     79                 } else {
     80                     return false;
     81                 }
     82             }
     83 
     84             // Initialize variables.
     85             final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
     86             final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
     87             final int countX = cellLayout.getCountX();
     88             final int countY = cellLayout.getCountY();
     89 
     90             final int iconIndex = itemContainer.indexOfChild(v);
     91             final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
     92 
     93             final int pageIndex = pagedView.indexOfChild(cellLayout);
     94             final int pageCount = pagedView.getPageCount();
     95             final boolean isLayoutRtl = Utilities.isRtl(v.getResources());
     96 
     97             int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
     98             // Process focus.
     99             int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
    100                     countY, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl);
    101             if (newIconIndex == FocusLogic.NOOP) {
    102                 handleNoopKey(keyCode, v);
    103                 return consume;
    104             }
    105             ShortcutAndWidgetContainer newParent = null;
    106             View child = null;
    107 
    108             switch (newIconIndex) {
    109                 case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
    110                 case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
    111                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
    112                     if (newParent != null) {
    113                         int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
    114                         pagedView.snapToPage(pageIndex - 1);
    115                         child = newParent.getChildAt(
    116                                 ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
    117                                     ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row);
    118                     }
    119                     break;
    120                 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
    121                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
    122                     if (newParent != null) {
    123                         pagedView.snapToPage(pageIndex - 1);
    124                         child = newParent.getChildAt(0, 0);
    125                     }
    126                     break;
    127                 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
    128                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
    129                     if (newParent != null) {
    130                         pagedView.snapToPage(pageIndex - 1);
    131                         child = newParent.getChildAt(countX - 1, countY - 1);
    132                     }
    133                     break;
    134                 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
    135                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
    136                     if (newParent != null) {
    137                         pagedView.snapToPage(pageIndex + 1);
    138                         child = newParent.getChildAt(0, 0);
    139                     }
    140                     break;
    141                 case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
    142                 case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
    143                     newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
    144                     if (newParent != null) {
    145                         pagedView.snapToPage(pageIndex + 1);
    146                         child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex);
    147                     }
    148                     break;
    149                 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
    150                     child = cellLayout.getChildAt(0, 0);
    151                     break;
    152                 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
    153                     child = pagedView.getLastItem();
    154                     break;
    155                 default: // Go to some item on the current page.
    156                     child = itemContainer.getChildAt(newIconIndex);
    157                     break;
    158             }
    159             if (child != null) {
    160                 child.requestFocus();
    161                 playSoundEffect(keyCode, v);
    162             } else {
    163                 handleNoopKey(keyCode, v);
    164             }
    165             return consume;
    166         }
    167 
    168         public void handleNoopKey(int keyCode, View v) {
    169             if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
    170                 mFolder.mFolderName.requestFocus();
    171                 playSoundEffect(keyCode, v);
    172             }
    173         }
    174     }
    175 
    176     /**
    177      * Handles key events in the workspace hot seat (bottom of the screen).
    178      * <p>Currently we don't special case for the phone UI in different orientations, even though
    179      * the hotseat is on the side in landscape mode. This is to ensure that accessibility
    180      * consistency is maintained across rotations.
    181      */
    182     static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
    183         boolean consume = FocusLogic.shouldConsume(keyCode);
    184         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
    185             return consume;
    186         }
    187 
    188         DeviceProfile profile = ((Launcher) v.getContext()).getDeviceProfile();
    189 
    190         if (DEBUG) {
    191             Log.v(TAG, String.format(
    192                     "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
    193                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
    194         }
    195 
    196         // Initialize the variables.
    197         final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
    198         final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
    199         Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
    200 
    201         Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
    202         int pageIndex = workspace.getNextPage();
    203         int pageCount = workspace.getChildCount();
    204         int countX = -1;
    205         int countY = -1;
    206         int iconIndex = hotseatParent.indexOfChild(v);
    207         int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
    208                 .getChildAt(iconIndex).getLayoutParams()).cellX;
    209 
    210         final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
    211         if (iconLayout == null) {
    212             // This check is to guard against cases where key strokes rushes in when workspace
    213             // child creation/deletion is still in flux. (e.g., during drop or fling
    214             // animation.)
    215             return consume;
    216         }
    217         final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
    218 
    219         ViewGroup parent = null;
    220         int[][] matrix = null;
    221 
    222         if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
    223                 !profile.isVerticalBarLayout()) {
    224             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
    225                     true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
    226                     iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
    227             iconIndex += iconParent.getChildCount();
    228             countX = iconLayout.getCountX();
    229             countY = iconLayout.getCountY() + hotseatLayout.getCountY();
    230             parent = iconParent;
    231         } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
    232                 profile.isVerticalBarLayout()) {
    233             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
    234                     false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
    235                     iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
    236             iconIndex += iconParent.getChildCount();
    237             countX = iconLayout.getCountX() + hotseatLayout.getCountX();
    238             countY = iconLayout.getCountY();
    239             parent = iconParent;
    240         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
    241                 profile.isVerticalBarLayout()) {
    242             keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
    243         }else {
    244             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
    245             // matrix extended with hotseat.
    246             matrix = FocusLogic.createSparseMatrix(hotseatLayout);
    247             countX = hotseatLayout.getCountX();
    248             countY = hotseatLayout.getCountY();
    249             parent = hotseatParent;
    250         }
    251 
    252         // Process the focus.
    253         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
    254                 countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources()));
    255 
    256         View newIcon = null;
    257         if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
    258             parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
    259             newIcon = parent.getChildAt(0);
    260             // TODO(hyunyoungs): handle cases where the child is not an icon but
    261             // a folder or a widget.
    262             workspace.snapToPage(pageIndex + 1);
    263         }
    264         if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
    265             newIconIndex -= iconParent.getChildCount();
    266         }
    267         if (parent != null) {
    268             if (newIcon == null && newIconIndex >=0) {
    269                 newIcon = parent.getChildAt(newIconIndex);
    270             }
    271             if (newIcon != null) {
    272                 newIcon.requestFocus();
    273                 playSoundEffect(keyCode, v);
    274             }
    275         }
    276         return consume;
    277     }
    278 
    279     /**
    280      * Handles key events in a workspace containing icons.
    281      */
    282     static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
    283         boolean consume = FocusLogic.shouldConsume(keyCode);
    284         if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
    285             return consume;
    286         }
    287 
    288         Launcher launcher = (Launcher) v.getContext();
    289         DeviceProfile profile = launcher.getDeviceProfile();
    290 
    291         if (DEBUG) {
    292             Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
    293                     KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
    294         }
    295 
    296         // Initialize the variables.
    297         ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
    298         CellLayout iconLayout = (CellLayout) parent.getParent();
    299         final Workspace workspace = (Workspace) iconLayout.getParent();
    300         final ViewGroup dragLayer = (ViewGroup) workspace.getParent();
    301         final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar);
    302         final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
    303 
    304         final int iconIndex = parent.indexOfChild(v);
    305         final int pageIndex = workspace.indexOfChild(iconLayout);
    306         final int pageCount = workspace.getChildCount();
    307         int countX = iconLayout.getCountX();
    308         int countY = iconLayout.getCountY();
    309 
    310         CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
    311         ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
    312         int[][] matrix;
    313 
    314         // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
    315         // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
    316         // with the hotseat.
    317         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
    318             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
    319                     profile.inv.hotseatAllAppsRank,
    320                     !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
    321             countY = countY + 1;
    322         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
    323                 profile.isVerticalBarLayout()) {
    324             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
    325                     profile.inv.hotseatAllAppsRank,
    326                     !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
    327             countX = countX + 1;
    328         } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
    329             workspace.removeWorkspaceItem(v);
    330             return consume;
    331         } else {
    332             matrix = FocusLogic.createSparseMatrix(iconLayout);
    333         }
    334 
    335         // Process the focus.
    336         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
    337                 countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources()));
    338         View newIcon = null;
    339         switch (newIconIndex) {
    340             case FocusLogic.NOOP:
    341                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
    342                     newIcon = tabs;
    343                 }
    344                 break;
    345             case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
    346             case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
    347                 int newPageIndex = pageIndex - 1;
    348                 if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
    349                     newPageIndex = pageIndex + 1;
    350                 }
    351                 int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
    352                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
    353                 workspace.snapToPage(newPageIndex);
    354                 if (parent != null) {
    355                     workspace.snapToPage(newPageIndex);
    356                     iconLayout = (CellLayout) parent.getParent();
    357                     matrix = FocusLogic.createSparseMatrix(iconLayout,
    358                         iconLayout.getCountX(), row);
    359                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
    360                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
    361                             Utilities.isRtl(v.getResources()));
    362                     newIcon = parent.getChildAt(newIconIndex);
    363                 }
    364                 break;
    365             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
    366                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
    367                 newIcon = parent.getChildAt(0);
    368                 workspace.snapToPage(pageIndex - 1);
    369                 break;
    370             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
    371                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
    372                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
    373                 workspace.snapToPage(pageIndex - 1);
    374                 break;
    375             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
    376                 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
    377                 newIcon = parent.getChildAt(0);
    378                 workspace.snapToPage(pageIndex + 1);
    379                 break;
    380             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
    381             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
    382                 newPageIndex = pageIndex + 1;
    383                 if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
    384                     newPageIndex = pageIndex - 1;
    385                 }
    386                 workspace.snapToPage(newPageIndex);
    387                 row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
    388                 parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
    389                 if (parent != null) {
    390                     workspace.snapToPage(newPageIndex);
    391                     iconLayout = (CellLayout) parent.getParent();
    392                     matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
    393                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
    394                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
    395                             Utilities.isRtl(v.getResources()));
    396                     newIcon = parent.getChildAt(newIconIndex);
    397                 }
    398                 break;
    399             case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
    400                 newIcon = parent.getChildAt(0);
    401                 break;
    402             case FocusLogic.CURRENT_PAGE_LAST_ITEM:
    403                 newIcon = parent.getChildAt(parent.getChildCount() - 1);
    404                 break;
    405             default:
    406                 // current page, some item.
    407                 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
    408                     newIcon = parent.getChildAt(newIconIndex);
    409                 } else if (parent.getChildCount() <= newIconIndex &&
    410                         newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
    411                     newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
    412                 }
    413                 break;
    414         }
    415         if (newIcon != null) {
    416             newIcon.requestFocus();
    417             playSoundEffect(keyCode, v);
    418         }
    419         return consume;
    420     }
    421 
    422     //
    423     // Helper methods.
    424     //
    425 
    426     /**
    427      * Private helper method to get the CellLayoutChildren given a CellLayout index.
    428      */
    429     @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
    430             ViewGroup container, int i) {
    431         CellLayout parent = (CellLayout) container.getChildAt(i);
    432         return parent.getShortcutsAndWidgets();
    433     }
    434 
    435     /**
    436      * Helper method to be used for playing sound effects.
    437      */
    438     @Thunk static void playSoundEffect(int keyCode, View v) {
    439         switch (keyCode) {
    440             case KeyEvent.KEYCODE_DPAD_LEFT:
    441                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
    442                 break;
    443             case KeyEvent.KEYCODE_DPAD_RIGHT:
    444                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
    445                 break;
    446             case KeyEvent.KEYCODE_DPAD_DOWN:
    447             case KeyEvent.KEYCODE_PAGE_DOWN:
    448             case KeyEvent.KEYCODE_MOVE_END:
    449                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
    450                 break;
    451             case KeyEvent.KEYCODE_DPAD_UP:
    452             case KeyEvent.KEYCODE_PAGE_UP:
    453             case KeyEvent.KEYCODE_MOVE_HOME:
    454                 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
    455                 break;
    456             default:
    457                 break;
    458         }
    459     }
    460 }
    461