/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.util.Log; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderPagedView; import com.android.launcher3.util.FocusLogic; import com.android.launcher3.util.Thunk; /** * A keyboard listener we set on all the workspace icons. */ class IconKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleIconKeyEvent(v, keyCode, event); } } /** * A keyboard listener we set on all the hotseat buttons. */ class HotseatIconKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event); } } /** * A keyboard listener we set on full screen pages (e.g. custom content). */ class FullscreenKeyEventListener implements View.OnKeyListener { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_PAGE_UP) { // Handle the key event just like a workspace icon would in these cases. In this case, // it will basically act as if there is a single icon in the top left (so you could // think of the fullscreen page as a focusable fullscreen widget). return FocusHelper.handleIconKeyEvent(v, keyCode, event); } return false; } } public class FocusHelper { private static final String TAG = "FocusHelper"; private static final boolean DEBUG = false; /** * Handles key events in paged folder. */ public static class PagedFolderKeyEventListener implements View.OnKeyListener { private final Folder mFolder; public PagedFolderKeyEventListener(Folder folder) { mFolder = folder; } @Override public boolean onKey(View v, int keyCode, KeyEvent e) { boolean consume = FocusLogic.shouldConsume(keyCode); if (e.getAction() == KeyEvent.ACTION_UP) { return consume; } if (DEBUG) { Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].", KeyEvent.keyCodeToString(keyCode))); } if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { if (ProviderConfig.IS_DOGFOOD_BUILD) { throw new IllegalStateException("Parent of the focused item is not supported."); } else { return false; } } // Initialize variables. final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout cellLayout = (CellLayout) itemContainer.getParent(); final int iconIndex = itemContainer.indexOfChild(v); final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent(); final int pageIndex = pagedView.indexOfChild(cellLayout); final int pageCount = pagedView.getPageCount(); final boolean isLayoutRtl = Utilities.isRtl(v.getResources()); int[][] matrix = FocusLogic.createSparseMatrix(cellLayout); // Process focus. int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, pageCount, isLayoutRtl); if (newIconIndex == FocusLogic.NOOP) { handleNoopKey(keyCode, v); return consume; } ShortcutAndWidgetContainer newParent = null; View child = null; switch (newIconIndex) { case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); if (newParent != null) { int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; pagedView.snapToPage(pageIndex - 1); child = newParent.getChildAt( ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) ^ newParent.invertLayoutHorizontally()) ? 0 : matrix.length - 1, row); } break; case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); if (newParent != null) { pagedView.snapToPage(pageIndex - 1); child = newParent.getChildAt(0, 0); } break; case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1); if (newParent != null) { pagedView.snapToPage(pageIndex - 1); child = newParent.getChildAt(matrix.length - 1, matrix[0].length - 1); } break; case FocusLogic.NEXT_PAGE_FIRST_ITEM: newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); if (newParent != null) { pagedView.snapToPage(pageIndex + 1); child = newParent.getChildAt(0, 0); } break; case FocusLogic.NEXT_PAGE_LEFT_COLUMN: case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1); if (newParent != null) { pagedView.snapToPage(pageIndex + 1); child = FocusLogic.getAdjacentChildInNextFolderPage( newParent, v, newIconIndex); } break; case FocusLogic.CURRENT_PAGE_FIRST_ITEM: child = cellLayout.getChildAt(0, 0); break; case FocusLogic.CURRENT_PAGE_LAST_ITEM: child = pagedView.getLastItem(); break; default: // Go to some item on the current page. child = itemContainer.getChildAt(newIconIndex); break; } if (child != null) { child.requestFocus(); playSoundEffect(keyCode, v); } else { handleNoopKey(keyCode, v); } return consume; } public void handleNoopKey(int keyCode, View v) { if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { mFolder.mFolderName.requestFocus(); playSoundEffect(keyCode, v); } } } /** * Handles key events in the workspace hotseat (bottom of the screen). *

Currently we don't special case for the phone UI in different orientations, even though * the hotseat is on the side in landscape mode. This is to ensure that accessibility * consistency is maintained across rotations. */ static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) { boolean consume = FocusLogic.shouldConsume(keyCode); if (e.getAction() == KeyEvent.ACTION_UP || !consume) { return consume; } final Launcher launcher = (Launcher) v.getContext(); final DeviceProfile profile = launcher.getDeviceProfile(); if (DEBUG) { Log.v(TAG, String.format( "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s", KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); } // Initialize the variables. final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace); final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent(); final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent(); final ItemInfo itemInfo = (ItemInfo) v.getTag(); int pageIndex = workspace.getNextPage(); int pageCount = workspace.getChildCount(); int iconIndex = hotseatParent.indexOfChild(v); int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets() .getChildAt(iconIndex).getLayoutParams()).cellX; final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex); if (iconLayout == null) { // This check is to guard against cases where key strokes rushes in when workspace // child creation/deletion is still in flux. (e.g., during drop or fling // animation.) return consume; } final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); ViewGroup parent = null; int[][] matrix = null; if (keyCode == KeyEvent.KEYCODE_DPAD_UP && !profile.isVerticalBarLayout()) { matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile); iconIndex += iconParent.getChildCount(); parent = iconParent; } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && profile.isVerticalBarLayout()) { matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile); iconIndex += iconParent.getChildCount(); parent = iconParent; } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && profile.isVerticalBarLayout()) { keyCode = KeyEvent.KEYCODE_PAGE_DOWN; } else if (isUninstallKeyChord(e)) { matrix = FocusLogic.createSparseMatrix(iconLayout); if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) { UninstallDropTarget.startUninstallActivity(launcher, itemInfo); } } else if (isDeleteKeyChord(e)) { matrix = FocusLogic.createSparseMatrix(iconLayout); launcher.removeItem(v, itemInfo, true /* deleteFromDb */); } else { // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the // matrix extended with hotseat. matrix = FocusLogic.createSparseMatrix(hotseatLayout); parent = hotseatParent; } // Process the focus. int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); View newIcon = null; switch (newIconIndex) { case FocusLogic.NEXT_PAGE_FIRST_ITEM: parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1); newIcon = parent.getChildAt(0); // TODO(hyunyoungs): handle cases where the child is not an icon but // a folder or a widget. workspace.snapToPage(pageIndex + 1); break; case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); newIcon = parent.getChildAt(0); // TODO(hyunyoungs): handle cases where the child is not an icon but // a folder or a widget. workspace.snapToPage(pageIndex - 1); break; case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1); newIcon = parent.getChildAt(parent.getChildCount() - 1); // TODO(hyunyoungs): handle cases where the child is not an icon but // a folder or a widget. workspace.snapToPage(pageIndex - 1); break; case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: // Go to the previous page but keep the focus on the same hotseat icon. workspace.snapToPage(pageIndex - 1); // If the page we are going to is fullscreen, have it take the focus from hotseat. CellLayout prevPage = (CellLayout) workspace.getPageAt(pageIndex - 1); boolean isPrevPageFullscreen = ((CellLayout.LayoutParams) prevPage .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen; if (isPrevPageFullscreen) { workspace.getPageAt(pageIndex - 1).requestFocus(); } break; case FocusLogic.NEXT_PAGE_LEFT_COLUMN: case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: // Go to the next page but keep the focus on the same hotseat icon. workspace.snapToPage(pageIndex + 1); // If the page we are going to is fullscreen, have it take the focus from hotseat. CellLayout nextPage = (CellLayout) workspace.getPageAt(pageIndex + 1); boolean isNextPageFullscreen = ((CellLayout.LayoutParams) nextPage .getShortcutsAndWidgets().getChildAt(0).getLayoutParams()).isFullscreen; if (isNextPageFullscreen) { workspace.getPageAt(pageIndex + 1).requestFocus(); } break; } if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) { newIconIndex -= iconParent.getChildCount(); } if (parent != null) { if (newIcon == null && newIconIndex >= 0) { newIcon = parent.getChildAt(newIconIndex); } if (newIcon != null) { newIcon.requestFocus(); playSoundEffect(keyCode, v); } } return consume; } /** * Handles key events in a workspace containing icons. */ static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) { boolean consume = FocusLogic.shouldConsume(keyCode); if (e.getAction() == KeyEvent.ACTION_UP || !consume) { return consume; } Launcher launcher = (Launcher) v.getContext(); DeviceProfile profile = launcher.getDeviceProfile(); if (DEBUG) { Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s", KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout())); } // Initialize the variables. ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent(); CellLayout iconLayout = (CellLayout) parent.getParent(); final Workspace workspace = (Workspace) iconLayout.getParent(); final ViewGroup dragLayer = (ViewGroup) workspace.getParent(); final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.drop_target_bar); final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat); final ItemInfo itemInfo = (ItemInfo) v.getTag(); final int iconIndex = parent.indexOfChild(v); final int pageIndex = workspace.indexOfChild(iconLayout); final int pageCount = workspace.getChildCount(); CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0); ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets(); int[][] matrix; // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended // with the hotseat. if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) { matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && profile.isVerticalBarLayout()) { matrix = FocusLogic.createSparseMatrixWithHotseat(iconLayout, hotseatLayout, profile); } else if (isUninstallKeyChord(e)) { matrix = FocusLogic.createSparseMatrix(iconLayout); if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) { UninstallDropTarget.startUninstallActivity(launcher, itemInfo); } } else if (isDeleteKeyChord(e)) { matrix = FocusLogic.createSparseMatrix(iconLayout); launcher.removeItem(v, itemInfo, true /* deleteFromDb */); } else { matrix = FocusLogic.createSparseMatrix(iconLayout); } // Process the focus. int newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources())); boolean isRtl = Utilities.isRtl(v.getResources()); View newIcon = null; CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex); switch (newIconIndex) { case FocusLogic.NOOP: if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { newIcon = tabs; } break; case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN: case FocusLogic.NEXT_PAGE_RIGHT_COLUMN: int newPageIndex = pageIndex - 1; if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) { newPageIndex = pageIndex + 1; } int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); if (parent != null) { iconLayout = (CellLayout) parent.getParent(); matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, iconLayout.getCountX(), row); newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT, newPageIndex, pageCount, Utilities.isRtl(v.getResources())); if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl); } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) { newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl); } else { newIcon = parent.getChildAt(newIconIndex); } } break; case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM: workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1); newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); if (newIcon == null) { // Check the hotseat if no focusable item was found on the workspace. newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); workspace.snapToPage(pageIndex - 1); } break; case FocusLogic.PREVIOUS_PAGE_LAST_ITEM: newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl); break; case FocusLogic.NEXT_PAGE_FIRST_ITEM: newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl); break; case FocusLogic.NEXT_PAGE_LEFT_COLUMN: case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN: newPageIndex = pageIndex + 1; if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) { newPageIndex = pageIndex - 1; } row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY; parent = getCellLayoutChildrenForIndex(workspace, newPageIndex); if (parent != null) { iconLayout = (CellLayout) parent.getParent(); matrix = FocusLogic.createSparseMatrixWithPivotColumn(iconLayout, -1, row); newIconIndex = FocusLogic.handleKeyEvent(keyCode, matrix, FocusLogic.PIVOT, newPageIndex, pageCount, Utilities.isRtl(v.getResources())); if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) { newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl); } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) { newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl); } else { newIcon = parent.getChildAt(newIconIndex); } } break; case FocusLogic.CURRENT_PAGE_FIRST_ITEM: newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); if (newIcon == null) { // Check the hotseat if no focusable item was found on the workspace. newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); } break; case FocusLogic.CURRENT_PAGE_LAST_ITEM: newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl); if (newIcon == null) { // Check the hotseat if no focusable item was found on the workspace. newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl); } break; default: // current page, some item. if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) { newIcon = parent.getChildAt(newIconIndex); } else if (parent.getChildCount() <= newIconIndex && newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) { newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount()); } break; } if (newIcon != null) { newIcon.requestFocus(); playSoundEffect(keyCode, v); } return consume; } // // Helper methods. // /** * Private helper method to get the CellLayoutChildren given a CellLayout index. */ @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex( ViewGroup container, int i) { CellLayout parent = (CellLayout) container.getChildAt(i); return parent.getShortcutsAndWidgets(); } /** * Helper method to be used for playing sound effects. */ @Thunk static void playSoundEffect(int keyCode, View v) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); break; case KeyEvent.KEYCODE_DPAD_RIGHT: v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); break; case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_PAGE_DOWN: case KeyEvent.KEYCODE_MOVE_END: v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN); break; case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_MOVE_HOME: v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP); break; default: break; } } /** * Returns whether the key event represents a valid uninstall key chord. */ private static boolean isUninstallKeyChord(KeyEvent event) { int keyCode = event.getKeyCode(); return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) && event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON); } /** * Returns whether the key event represents a valid delete key chord. */ private static boolean isDeleteKeyChord(KeyEvent event) { int keyCode = event.getKeyCode(); return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) && event.hasModifiers(KeyEvent.META_CTRL_ON); } private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl) { if (pageIndex - 1 < 0) { return null; } CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1); View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl); if (newIcon == null) { // Check the hotseat if no focusable item was found on the workspace. newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl); workspace.snapToPage(pageIndex - 1); } return newIcon; } private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout, int pageIndex, boolean isRtl) { if (pageIndex + 1 >= workspace.getPageCount()) { return null; } CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1); View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl); if (newIcon == null) { // Check the hotseat if no focusable item was found on the workspace. newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl); workspace.snapToPage(pageIndex + 1); } return newIcon; } private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) { View icon; int countX = cellLayout.getCountX(); for (int y = 0; y < cellLayout.getCountY(); y++) { int increment = isRtl ? -1 : 1; for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) { if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) { return icon; } } } return null; } private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout, boolean isRtl) { View icon; int countX = cellLayout.getCountX(); for (int y = cellLayout.getCountY() - 1; y >= 0; y--) { int increment = isRtl ? 1 : -1; for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) { if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) { return icon; } } } return null; } }