blob: f07040db03cd1ee9a4173ed619cda8936f22bcac [file] [log] [blame]
/*
* 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.FeatureFlags;
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;
}
}
/**
* TODO: Reevaluate if this is still required
*/
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 (FeatureFlags.IS_STUDIO_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).
* <p>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.getLauncher(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 {
// 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);
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);
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.getLauncher(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 {
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;
}
}
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;
}
}