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 onKey(View v, int keyCode, KeyEvent event)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 onKey(View v, int keyCode, KeyEvent event)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 PagedFolderKeyEventListener(Folder folder)60 public PagedFolderKeyEventListener(Folder folder) { 61 mFolder = folder; 62 } 63 64 @Override onKey(View v, int keyCode, KeyEvent e)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 handleNoopKey(int keyCode, View v)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 */ handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e)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 */ handleIconKeyEvent(View v, int keyCode, KeyEvent e)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 */ getCellLayoutChildrenForIndex( ViewGroup container, int i)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 */ playSoundEffect(int keyCode, View v)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