blob: c02d73cb69e9dfe04d60f9257a35653a59518818 [file] [log] [blame]
Winson Chung97d85d22011-04-13 11:27:36 -07001/*
Hyunyoung Songee3e6a72015-02-20 14:25:27 -08002 * Copyright (C) 2015 The Android Open Source Project
Winson Chung97d85d22011-04-13 11:27:36 -07003 *
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
Daniel Sandler325dc232013-06-05 22:57:57 -040017package com.android.launcher3;
Winson Chung97d85d22011-04-13 11:27:36 -070018
Winson Chung4e6a9762011-05-09 11:56:34 -070019import android.content.res.Configuration;
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080020import android.util.Log;
Winson Chung97d85d22011-04-13 11:27:36 -070021import android.view.KeyEvent;
Sunny Goyalb3726d92014-08-20 16:58:17 -070022import android.view.SoundEffectConstants;
Winson Chung97d85d22011-04-13 11:27:36 -070023import android.view.View;
24import android.view.ViewGroup;
Winson Chunga1f133d2013-07-25 11:14:30 -070025import android.widget.ScrollView;
Winson Chung97d85d22011-04-13 11:27:36 -070026
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080027import com.android.launcher3.util.FocusLogic;
Winson Chungfaa13252011-06-13 18:15:54 -070028
Winson Chung97d85d22011-04-13 11:27:36 -070029/**
Winson Chung4d279d92011-07-21 11:46:32 -070030 * A keyboard listener we set on all the workspace icons.
31 */
Adam Cohenac56cff2011-09-28 20:45:37 -070032class IconKeyEventListener implements View.OnKeyListener {
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080033 @Override
Winson Chung4d279d92011-07-21 11:46:32 -070034 public boolean onKey(View v, int keyCode, KeyEvent event) {
Adam Cohenac56cff2011-09-28 20:45:37 -070035 return FocusHelper.handleIconKeyEvent(v, keyCode, event);
36 }
37}
38
39/**
40 * A keyboard listener we set on all the workspace icons.
41 */
42class FolderKeyEventListener implements View.OnKeyListener {
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080043 @Override
Adam Cohenac56cff2011-09-28 20:45:37 -070044 public boolean onKey(View v, int keyCode, KeyEvent event) {
45 return FocusHelper.handleFolderKeyEvent(v, keyCode, event);
Winson Chung4d279d92011-07-21 11:46:32 -070046 }
47}
48
49/**
Winson Chung3d503fb2011-07-13 17:25:49 -070050 * A keyboard listener we set on all the hotseat buttons.
Winson Chung4e6a9762011-05-09 11:56:34 -070051 */
Adam Cohenac56cff2011-09-28 20:45:37 -070052class HotseatIconKeyEventListener implements View.OnKeyListener {
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080053 @Override
Winson Chung4e6a9762011-05-09 11:56:34 -070054 public boolean onKey(View v, int keyCode, KeyEvent event) {
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080055 return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
Winson Chung4e6a9762011-05-09 11:56:34 -070056 }
57}
58
Winson Chung97d85d22011-04-13 11:27:36 -070059public class FocusHelper {
Winson Chung97d85d22011-04-13 11:27:36 -070060
Hyunyoung Songee3e6a72015-02-20 14:25:27 -080061 private static final String TAG = "FocusHelper";
62 private static final boolean DEBUG = false;
63
64 //
65 // Key code handling methods.
66 //
67
68 /**
69 * Handles key events in the all apps screen.
70 */
71 static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
72 boolean consume = FocusLogic.shouldConsume(keyCode);
73 if (e.getAction() == KeyEvent.ACTION_UP) {
74 return consume;
75 }
76 if (DEBUG) {
77 Log.v(TAG, String.format("Handle ALL APPS keyevent=[%s].",
78 KeyEvent.keyCodeToString(keyCode)));
79 }
80
81 // Initialize variables.
82 ViewGroup parentLayout;
83 ViewGroup itemContainer;
84 int countX;
85 int countY;
86 if (v.getParent() instanceof ShortcutAndWidgetContainer) {
87 itemContainer = (ViewGroup) v.getParent();
88 parentLayout = (ViewGroup) itemContainer.getParent();
89 countX = ((CellLayout) parentLayout).getCountX();
90 countY = ((CellLayout) parentLayout).getCountY();
91 } else if (v.getParent() instanceof ViewGroup) {
92 itemContainer = parentLayout = (ViewGroup) v.getParent();
93 countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
94 countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
95 } else {
96 throw new IllegalStateException(
97 "Parent of the focused item inside all apps screen is not a supported type.");
98 }
99 final int iconIndex = itemContainer.indexOfChild(v);
100 final PagedView container = (PagedView) parentLayout.getParent();
101 final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout));
102 final int pageCount = container.getChildCount();
103 ViewGroup newParent = null;
104 View child = null;
105 int[][] matrix = FocusLogic.createFullMatrix(countX, countY, true);
106
107 // Process focus.
108 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
109 iconIndex, pageIndex, pageCount);
110 if (newIconIndex == FocusLogic.NOOP) {
111 return consume;
112 }
113 switch (newIconIndex) {
114 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
115 newParent = getAppsCustomizePage(container, pageIndex - 1);
116 if (newParent != null) {
117 container.snapToPage(pageIndex - 1);
118 child = newParent.getChildAt(0);
119 }
120 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
121 newParent = getAppsCustomizePage(container, pageIndex - 1);
122 if (newParent != null) {
123 container.snapToPage(pageIndex - 1);
124 child = newParent.getChildAt(newParent.getChildCount() - 1);
125 }
126 break;
127 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
128 newParent = getAppsCustomizePage(container, pageIndex + 1);
129 if (newParent != null) {
130 container.snapToPage(pageIndex + 1);
131 child = newParent.getChildAt(0);
132 }
133 break;
134 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
135 child = container.getChildAt(0);
136 break;
137 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
138 child = itemContainer.getChildAt(itemContainer.getChildCount() - 1);
139 break;
140 default: // Go to some item on the current page.
141 child = itemContainer.getChildAt(newIconIndex);
142 break;
143 }
144 if (child != null) {
145 child.requestFocus();
146 playSoundEffect(keyCode, v);
147 }
148 return consume;
149 }
150
151 /**
152 * Handles key events in the workspace hot seat (bottom of the screen).
153 * <p>Currently we don't special case for the phone UI in different orientations, even though
154 * the hotseat is on the side in landscape mode. This is to ensure that accessibility
155 * consistency is maintained across rotations.
156 */
157 static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
158 boolean consume = FocusLogic.shouldConsume(keyCode);
159 if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
160 return consume;
161 }
162 int orientation = v.getResources().getConfiguration().orientation;
163
164 if (DEBUG) {
165 Log.v(TAG, String.format(
166 "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, orientation=%d",
167 KeyEvent.keyCodeToString(keyCode), orientation));
168 }
169
170 // Initialize the variables.
171 final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
172 final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
173 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
174 int pageIndex = workspace.getCurrentPage();
175 int pageCount = workspace.getChildCount();
176 int countX, countY;
177 int iconIndex = findIndexOfView(hotseatParent, v);
178
179 final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
180 final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
181
182 ViewGroup parent = null;
183 int[][] matrix;
184
185 if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
186 orientation == Configuration.ORIENTATION_PORTRAIT) {
187 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation);
188 // TODO: hotseat indexing should be symmetric.
189 iconIndex += iconParent.getChildCount();
190 countX = iconLayout.getCountX();
191 countY = iconLayout.getCountY() + hotseatLayout.getCountY();
192 parent = iconParent;
193 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
194 orientation == Configuration.ORIENTATION_LANDSCAPE) {
195 matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, orientation);
196 // TODO: hotseat indexing should be symmetric.
197 iconIndex += iconParent.getChildCount();
198 countX = iconLayout.getCountX() + hotseatLayout.getCountX();
199 countY = iconLayout.getCountY();
200 parent = iconParent;
201 } else {
202 // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
203 // matrix extended with hotseat.
204 matrix = FocusLogic.createSparseMatrix(hotseatLayout);
205 parent = hotseatParent;
206 countX = hotseatLayout.getCountX();
207 countY = hotseatLayout.getCountY();
208
209 }
210
211 // Process the focus.
212 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
213 iconIndex, pageIndex, pageCount);
214
215 if (iconParent.getChildCount() <= newIconIndex &&
216 newIconIndex < iconParent.getChildCount() + hotseatParent.getChildCount()) {
217 newIconIndex -= iconParent.getChildCount();
218 }
219 if (parent != null) {
220 View newIcon = parent.getChildAt(newIconIndex);
221 if (newIcon != null) {
222 newIcon.requestFocus();
223 playSoundEffect(keyCode, v);
224 }
225 }
226 return consume;
227 }
228
229 /**
230 * Handles key events in a workspace containing icons.
231 */
232 static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
233 boolean consume = FocusLogic.shouldConsume(keyCode);
234 if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
235 return consume;
236 }
237 int orientation = v.getResources().getConfiguration().orientation;
238 if (DEBUG) {
239 Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] orientation=%d",
240 KeyEvent.keyCodeToString(keyCode), orientation));
241 }
242
243 // Initialize the variables.
244 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
245 final CellLayout layout = (CellLayout) parent.getParent();
246 final Workspace workspace = (Workspace) layout.getParent();
247 final ViewGroup launcher = (ViewGroup) workspace.getParent();
248 final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
249 final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat);
250 int pageIndex = workspace.indexOfChild(layout);
251 int pageCount = workspace.getChildCount();
252 final int countX = layout.getCountX();
253 int countY = layout.getCountY();
254 final int iconIndex = findIndexOfView(parent, v);
255
256 CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
257 ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
258 int[][] matrix;
259
260 // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_LEFT in landscape) is the only key allowed
261 // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
262 // with the hotseat.
263 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN &&
264 orientation == Configuration.ORIENTATION_PORTRAIT) {
265 matrix = FocusLogic.createSparseMatrix(layout, hotseatLayout, orientation);
266 countY = countY + 1;
267 } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
268 orientation == Configuration.ORIENTATION_LANDSCAPE) {
269 matrix = FocusLogic.createSparseMatrix(layout, hotseatLayout, orientation);
270 countY = countY + 1;
271 } else {
272 matrix = FocusLogic.createSparseMatrix(layout);
273 }
274
275 // Process the focus.
276 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
277 iconIndex, pageIndex, pageCount);
278 View newIcon = null;
279 switch (newIconIndex) {
280 case FocusLogic.NOOP:
281 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
282 newIcon = tabs;
283 }
284 break;
285 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
286 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
287 newIcon = parent.getChildAt(0);
288 workspace.snapToPage(pageIndex - 1);
289 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
290 parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
291 newIcon = parent.getChildAt(parent.getChildCount() - 1);
292 workspace.snapToPage(pageIndex - 1);
293 break;
294 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
295 parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
296 newIcon = parent.getChildAt(0);
297 workspace.snapToPage(pageIndex - 1);
298 break;
299 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
300 newIcon = parent.getChildAt(0);
301 break;
302 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
303 newIcon = parent.getChildAt(parent.getChildCount() - 1);
304 break;
305 default:
306 // current page, some item.
307 if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
308 newIcon = parent.getChildAt(newIconIndex);
309 } else if (parent.getChildCount() <= newIconIndex &&
310 newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
311 newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
312 }
313 break;
314 }
315 if (newIcon != null) {
316 newIcon.requestFocus();
317 playSoundEffect(keyCode, v);
318 }
319 return consume;
320 }
321
322 /**
323 * Handles key events for items in a Folder.
324 */
325 static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
326 boolean consume = FocusLogic.shouldConsume(keyCode);
327 if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
328 return consume;
329 }
330 if (DEBUG) {
331 Log.v(TAG, String.format("Handle FOLDER keyevent=[%s].",
332 KeyEvent.keyCodeToString(keyCode)));
333 }
334
335 // Initialize the variables.
336 ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
337 final CellLayout layout = (CellLayout) parent.getParent();
338 final ScrollView scrollView = (ScrollView) layout.getParent();
339 final Folder folder = (Folder) scrollView.getParent();
340 View title = folder.mFolderName;
341 Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
342 final int countX = layout.getCountX();
343 final int countY = layout.getCountY();
344 final int iconIndex = findIndexOfView(parent, v);
345 int pageIndex = workspace.indexOfChild(layout);
346 int pageCount = workspace.getChildCount();
347 int[][] map = FocusLogic.createFullMatrix(countX, countY, true /* incremental order index */
348 );
349
350 // Process the focus.
351 int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, map, iconIndex,
352 pageIndex, pageCount);
353 View newIcon = null;
354 switch (newIconIndex) {
355 case FocusLogic.NOOP:
356 if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
357 newIcon = title;
358 }
359 break;
360 case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
361 case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
362 case FocusLogic.NEXT_PAGE_FIRST_ITEM:
363 case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
364 case FocusLogic.CURRENT_PAGE_LAST_ITEM:
365 if (DEBUG) {
366 Log.v(TAG, "Page advance handling not supported on folder icons.");
367 }
368 break;
369 default: // current page some item.
370 newIcon = parent.getChildAt(newIconIndex);
371 break;
372 }
373 if (newIcon != null) {
374 newIcon.requestFocus();
375 playSoundEffect(keyCode, v);
376 }
377 return consume;
378 }
379
380 //
381 // Helper methods.
382 //
383
Winson Chung97d85d22011-04-13 11:27:36 -0700384 /**
Adam Cohenae4f1552011-10-20 00:15:42 -0700385 * Returns the Viewgroup containing page contents for the page at the index specified.
386 */
387 private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
388 ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
Winson Chungc58497e2013-09-03 17:48:37 -0700389 if (page instanceof CellLayout) {
Adam Cohenae4f1552011-10-20 00:15:42 -0700390 // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
Winson Chungc58497e2013-09-03 17:48:37 -0700391 page = ((CellLayout) page).getShortcutsAndWidgets();
Adam Cohenae4f1552011-10-20 00:15:42 -0700392 }
393 return page;
394 }
395
396 /**
Winson Chung97d85d22011-04-13 11:27:36 -0700397 * Private helper method to get the CellLayoutChildren given a CellLayout index.
398 */
Michael Jurkaa52570f2012-03-20 03:18:20 -0700399 private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
400 ViewGroup container, int i) {
Sunny Goyalb3726d92014-08-20 16:58:17 -0700401 CellLayout parent = (CellLayout) container.getChildAt(i);
402 return parent.getShortcutsAndWidgets();
Winson Chung97d85d22011-04-13 11:27:36 -0700403 }
404
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800405 private static int findIndexOfView(ViewGroup parent, View v) {
406 for (int i = 0; i < parent.getChildCount(); i++) {
407 if (v != null && v.equals(parent.getChildAt(i))) {
408 return i;
Winson Chung97d85d22011-04-13 11:27:36 -0700409 }
410 }
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800411 return -1;
Winson Chung97d85d22011-04-13 11:27:36 -0700412 }
413
414 /**
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800415 * Helper method to be used for playing sound effects.
Winson Chung97d85d22011-04-13 11:27:36 -0700416 */
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800417 private static void playSoundEffect(int keyCode, View v) {
Winson Chung97d85d22011-04-13 11:27:36 -0700418 switch (keyCode) {
419 case KeyEvent.KEYCODE_DPAD_LEFT:
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800420 v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
Winson Chung97d85d22011-04-13 11:27:36 -0700421 break;
422 case KeyEvent.KEYCODE_DPAD_RIGHT:
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800423 v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
Winson Chung97d85d22011-04-13 11:27:36 -0700424 break;
425 case KeyEvent.KEYCODE_DPAD_DOWN:
Winson Chung97d85d22011-04-13 11:27:36 -0700426 case KeyEvent.KEYCODE_PAGE_DOWN:
Winson Chung97d85d22011-04-13 11:27:36 -0700427 case KeyEvent.KEYCODE_MOVE_END:
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800428 v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
Adam Cohenac56cff2011-09-28 20:45:37 -0700429 break;
430 case KeyEvent.KEYCODE_DPAD_UP:
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800431 case KeyEvent.KEYCODE_PAGE_UP:
Adam Cohenac56cff2011-09-28 20:45:37 -0700432 case KeyEvent.KEYCODE_MOVE_HOME:
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800433 v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
Adam Cohenac56cff2011-09-28 20:45:37 -0700434 break;
Hyunyoung Songee3e6a72015-02-20 14:25:27 -0800435 default:
Winson Chung97d85d22011-04-13 11:27:36 -0700436 break;
Winson Chung97d85d22011-04-13 11:27:36 -0700437 }
Winson Chung97d85d22011-04-13 11:27:36 -0700438 }
439}