blob: 327fac46008b9140a3648c33b8567fd58d548e27 [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.content.res.Configuration;
import android.util.Log;
import android.view.KeyEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.FocusHelper.PagedViewKeyListener;
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 workspace icons.
*/
class FolderKeyEventListener implements View.OnKeyListener {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return FocusHelper.handleFolderKeyEvent(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);
}
}
public class FocusHelper {
private static final String TAG = "FocusHelper";
private static final boolean DEBUG = false;
//
// Key code handling methods.
//
/**
* A keyboard listener for scrollable folders
*/
public static class PagedFolderKeyEventListener extends PagedViewKeyListener {
private final Folder mFolder;
public PagedFolderKeyEventListener(Folder folder) {
mFolder = folder;
}
@Override
public void handleNoopKey(int keyCode, View v) {
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
mFolder.mFolderName.requestFocus();
playSoundEffect(keyCode, v);
}
}
}
/**
* Handles key events in the all apps screen.
*/
public static class PagedViewKeyListener implements View.OnKeyListener {
@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 APPS keyevent=[%s].",
KeyEvent.keyCodeToString(keyCode)));
}
// Initialize variables.
ViewGroup parentLayout;
ViewGroup itemContainer;
int countX;
int countY;
if (v.getParent() instanceof ShortcutAndWidgetContainer) {
itemContainer = (ViewGroup) v.getParent();
parentLayout = (ViewGroup) itemContainer.getParent();
countX = ((CellLayout) parentLayout).getCountX();
countY = ((CellLayout) parentLayout).getCountY();
} else {
if (LauncherAppState.isDogfoodBuild()) {
throw new IllegalStateException("Parent of the focused item is not supported.");
} else {
return false;
}
}
final int iconIndex = itemContainer.indexOfChild(v);
final PagedView container = (PagedView) parentLayout.getParent();
final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout));
final int pageCount = container.getChildCount();
ViewGroup newParent = null;
View child = null;
// TODO(hyunyoungs): this matrix is not applicable on the last page.
int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true);
// Process focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
iconIndex, pageIndex, pageCount);
if (newIconIndex == FocusLogic.NOOP) {
handleNoopKey(keyCode, v);
return consume;
}
switch (newIconIndex) {
case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
newParent = getAppsCustomizePage(container, pageIndex -1);
if (newParent != null) {
int row = FocusLogic.findRow(matrix, iconIndex);
container.snapToPage(pageIndex - 1);
// no need to create a new matrix.
child = newParent.getChildAt(matrix[countX-1][row]);
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
newParent = getAppsCustomizePage(container, pageIndex - 1);
if (newParent != null) {
container.snapToPage(pageIndex - 1);
child = newParent.getChildAt(0);
}
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
newParent = getAppsCustomizePage(container, pageIndex - 1);
if (newParent != null) {
container.snapToPage(pageIndex - 1);
child = newParent.getChildAt(newParent.getChildCount() - 1);
}
break;
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
newParent = getAppsCustomizePage(container, pageIndex + 1);
if (newParent != null) {
container.snapToPage(pageIndex + 1);
child = newParent.getChildAt(0);
}
break;
case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
newParent = getAppsCustomizePage(container, pageIndex + 1);
if (newParent != null) {
container.snapToPage(pageIndex + 1);
int row = FocusLogic.findRow(matrix, iconIndex);
child = newParent.getChildAt(matrix[0][row]);
}
break;
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
child = container.getChildAt(0);
break;
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
child = itemContainer.getChildAt(itemContainer.getChildCount() - 1);
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) { }
}
/**
* Handles key events in the workspace hot seat (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;
}
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile profile = app.getDynamicGrid().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 ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
int pageIndex = workspace.getCurrentPage();
int pageCount = workspace.getChildCount();
int countX = -1;
int countY = -1;
int iconIndex = findIndexOfView(hotseatParent, v);
int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
.getChildAt(iconIndex).getLayoutParams()).cellX;
final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
ViewGroup parent = null;
int[][] matrix = null;
if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
!profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
iconIndex += iconParent.getChildCount();
countX = iconLayout.getCountX();
countY = iconLayout.getCountY() + hotseatLayout.getCountY();
parent = iconParent;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
iconIndex += iconParent.getChildCount();
countX = iconLayout.getCountX() + hotseatLayout.getCountX();
countY = iconLayout.getCountY();
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);
countX = hotseatLayout.getCountX();
countY = hotseatLayout.getCountY();
parent = hotseatParent;
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
iconIndex, pageIndex, pageCount);
View newIcon = null;
if (newIconIndex == 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);
}
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;
}
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile profile = app.getDynamicGrid().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 launcher = (ViewGroup) workspace.getParent();
final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat);
int pageIndex = workspace.indexOfChild(iconLayout);
int pageCount = workspace.getChildCount();
int countX = iconLayout.getCountX();
int countY = iconLayout.getCountY();
final int iconIndex = findIndexOfView(parent, v);
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.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
countY = countY + 1;
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
profile.isVerticalBarLayout()) {
matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
countX = countX + 1;
} else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
workspace.removeWorkspaceItem(v);
return consume;
} else {
matrix = FocusLogic.createSparseMatrix(iconLayout);
}
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
iconIndex, pageIndex, pageCount);
View newIcon = null;
switch (newIconIndex) {
case FocusLogic.NOOP:
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
newIcon = tabs;
}
break;
case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
int row = FocusLogic.findRow(matrix, iconIndex);
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
if (parent != null) {
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrix(iconLayout,
iconLayout.getCountX(), row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
FocusLogic.PIVOT, pageIndex - 1, pageCount);
newIcon = parent.getChildAt(newIconIndex);
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(0);
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
newIcon = parent.getChildAt(parent.getChildCount() - 1);
workspace.snapToPage(pageIndex - 1);
break;
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
newIcon = parent.getChildAt(0);
workspace.snapToPage(pageIndex + 1);
break;
case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
row = FocusLogic.findRow(matrix, iconIndex);
parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
if (parent != null) {
iconLayout = (CellLayout) parent.getParent();
matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
FocusLogic.PIVOT, pageIndex, pageCount);
newIcon = parent.getChildAt(newIconIndex);
}
break;
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
newIcon = parent.getChildAt(0);
break;
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
newIcon = parent.getChildAt(parent.getChildCount() - 1);
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;
}
/**
* Handles key events for items in a Folder.
*/
static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
boolean consume = FocusLogic.shouldConsume(keyCode);
if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
return consume;
}
if (DEBUG) {
Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].",
KeyEvent.keyCodeToString(keyCode)));
}
// Initialize the variables.
ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
final CellLayout layout = (CellLayout) parent.getParent();
final Folder folder = (Folder) layout.getParent().getParent();
View title = folder.mFolderName;
Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
final int countX = layout.getCountX();
final int countY = layout.getCountY();
final int iconIndex = findIndexOfView(parent, v);
int pageIndex = workspace.indexOfChild(layout);
int pageCount = workspace.getChildCount();
int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order */);
// Process the focus.
int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex,
pageIndex, pageCount);
View newIcon = null;
switch (newIconIndex) {
case FocusLogic.NOOP:
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
newIcon = title;
}
break;
case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
case FocusLogic.NEXT_PAGE_FIRST_ITEM:
case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
case FocusLogic.CURRENT_PAGE_LAST_ITEM:
if (DEBUG) {
Log.v(TAG, "Page advance handling not supported on folder icons.");
}
break;
default: // current page some item.
newIcon = parent.getChildAt(newIconIndex);
break;
}
if (newIcon != null) {
newIcon.requestFocus();
playSoundEffect(keyCode, v);
}
return consume;
}
//
// Helper methods.
//
/**
* Returns the Viewgroup containing page contents for the page at the index specified.
*/
@Thunk static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
if (page instanceof CellLayout) {
// There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
page = ((CellLayout) page).getShortcutsAndWidgets();
}
return page;
}
/**
* Private helper method to get the CellLayoutChildren given a CellLayout index.
*/
private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
ViewGroup container, int i) {
CellLayout parent = (CellLayout) container.getChildAt(i);
return parent.getShortcutsAndWidgets();
}
private static int findIndexOfView(ViewGroup parent, View v) {
for (int i = 0; i < parent.getChildCount(); i++) {
if (v != null && v.equals(parent.getChildAt(i))) {
return i;
}
}
return -1;
}
/**
* 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;
}
}
}