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
17package com.android.launcher3.util;
18
19import android.util.Log;
20import android.view.KeyEvent;
21import android.view.View;
22import android.view.ViewGroup;
23
24import com.android.launcher3.CellLayout;
25import com.android.launcher3.DeviceProfile;
26import com.android.launcher3.Launcher;
27import com.android.launcher3.LauncherAppState;
28import com.android.launcher3.ShortcutAndWidgetContainer;
29
30import java.util.Arrays;
31
32/**
33 * Calculates the next item that a {@link KeyEvent} should change the focus to.
34 *<p>
35 * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates.
36 * Currently supports:
37 * <ul>
38 *  <li> full matrix of cells that are 1x1
39 *  <li> sparse matrix of cells that are 1x1
40 *     [ 1][  ][ 2][  ]
41 *     [  ][  ][ 3][  ]
42 *     [  ][ 4][  ][  ]
43 *     [  ][ 5][ 6][ 7]
44 * </ul>
45 * *<p>
46 * For testing, one can use a BT keyboard, or use following adb command.
47 * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT
48 */
49public class FocusLogic {
50
51    private static final String TAG = "FocusLogic";
52    private static final boolean DEBUG = false;
53
54    // Item and page index related constant used by {@link #handleKeyEvent}.
55    public static final int NOOP = -1;
56
57    public static final int PREVIOUS_PAGE_RIGHT_COLUMN  = -2;
58    public static final int PREVIOUS_PAGE_FIRST_ITEM    = -3;
59    public static final int PREVIOUS_PAGE_LAST_ITEM     = -4;
60    public static final int PREVIOUS_PAGE_LEFT_COLUMN   = -5;
61
62    public static final int CURRENT_PAGE_FIRST_ITEM     = -6;
63    public static final int CURRENT_PAGE_LAST_ITEM      = -7;
64
65    public static final int NEXT_PAGE_FIRST_ITEM        = -8;
66    public static final int NEXT_PAGE_LEFT_COLUMN       = -9;
67    public static final int NEXT_PAGE_RIGHT_COLUMN      = -10;
68
69    // Matrix related constant.
70    public static final int EMPTY = -1;
71    public static final int PIVOT = 100;
72
73    /**
74     * Returns true only if this utility class handles the key code.
75     */
76    public static boolean shouldConsume(int keyCode) {
77        return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
78                keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
79                keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END ||
80                keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN ||
81                keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL);
82    }
83
84    public static int handleKeyEvent(int keyCode, int cntX, int cntY,
85            int [][] map, int iconIdx, int pageIndex, int pageCount, boolean isRtl) {
86
87        if (DEBUG) {
88            Log.v(TAG, String.format(
89                    "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d",
90                    cntX, cntY, iconIdx, pageIndex, pageCount));
91        }
92
93        int newIndex = NOOP;
94        switch (keyCode) {
95            case KeyEvent.KEYCODE_DPAD_LEFT:
96                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/);
97                if (isRtl && newIndex == NOOP && pageIndex > 0) {
98                    newIndex = PREVIOUS_PAGE_RIGHT_COLUMN;
99                } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
100                    newIndex = NEXT_PAGE_RIGHT_COLUMN;
101                }
102                break;
103            case KeyEvent.KEYCODE_DPAD_RIGHT:
104                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/);
105                if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
106                    newIndex = NEXT_PAGE_LEFT_COLUMN;
107                } else if (isRtl && newIndex == NOOP && pageIndex > 0) {
108                    newIndex = PREVIOUS_PAGE_LEFT_COLUMN;
109                }
110                break;
111            case KeyEvent.KEYCODE_DPAD_DOWN:
112                newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1  /*increment*/);
113                break;
114            case KeyEvent.KEYCODE_DPAD_UP:
115                newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1  /*increment*/);
116                break;
117            case KeyEvent.KEYCODE_MOVE_HOME:
118                newIndex = handleMoveHome();
119                break;
120            case KeyEvent.KEYCODE_MOVE_END:
121                newIndex = handleMoveEnd();
122                break;
123            case KeyEvent.KEYCODE_PAGE_DOWN:
124                newIndex = handlePageDown(pageIndex, pageCount);
125                break;
126            case KeyEvent.KEYCODE_PAGE_UP:
127                newIndex = handlePageUp(pageIndex);
128                break;
129            default:
130                break;
131        }
132
133        if (DEBUG) {
134            Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]",
135                    iconIdx, getStringIndex(newIndex)));
136        }
137        return newIndex;
138    }
139
140    /**
141     * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}.
142     *
143     * @param m                 number of columns in the matrix
144     * @param n                 number of rows in the matrix
145     */
146    // TODO: get rid of dynamic matrix creation.
147    private static int[][] createFullMatrix(int m, int n) {
148        int[][] matrix = new int [m][n];
149
150        for (int i=0; i < m;i++) {
151            Arrays.fill(matrix[i], EMPTY);
152        }
153        return matrix;
154    }
155
156    /**
157     * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the
158     * index of the child view.
159     */
160    // TODO: get rid of the dynamic matrix creation
161    public static int[][] createSparseMatrix(CellLayout layout) {
162        ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets();
163        final int m = layout.getCountX();
164        final int n = layout.getCountY();
165        final boolean invert = parent.invertLayoutHorizontally();
166
167        int[][] matrix = createFullMatrix(m, n);
168
169        // Iterate thru the children.
170        for (int i = 0; i < parent.getChildCount(); i++ ) {
171            int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX;
172            int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY;
173            matrix[invert ? (m - cx - 1) : cx][cy] = i;
174        }
175        if (DEBUG) {
176            printMatrix(matrix);
177        }
178        return matrix;
179    }
180
181    /**
182     * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout.
183     * The size of the returning matrix is [icon column count x (icon + hotseat row count)]
184     * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)]
185     */
186    // TODO: get rid of the dynamic matrix creation
187    public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout,
188            boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) {
189
190        ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
191        ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets();
192
193        int m, n;
194        if (isHorizontal) {
195            m = iconLayout.getCountX();
196            n = iconLayout.getCountY() + hotseatLayout.getCountY();
197        } else {
198            m = iconLayout.getCountX() + hotseatLayout.getCountX();
199            n = iconLayout.getCountY();
200        }
201        int[][] matrix = createFullMatrix(m, n);
202
203        // Iterate thru the children of the top parent.
204        for (int i = 0; i < iconParent.getChildCount(); i++) {
205            int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX;
206            int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY;
207            matrix[cx][cy] = i;
208        }
209
210        // Iterate thru the children of the bottom parent
211        // The hotseat view group contains one more item than iconLayout column count.
212        // If {@param allappsiconRank} not negative, then the last icon in the hotseat
213        // is truncated. If it is negative, then all apps icon index is not inserted.
214        for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) {
215            int delta = 0;
216            if (isHorizontal) {
217                int cx = ((CellLayout.LayoutParams)
218                        hotseatParent.getChildAt(i).getLayoutParams()).cellX;
219                if ((includeAllappsicon && cx >= allappsiconRank) ||
220                        (!includeAllappsicon && cx > allappsiconRank)) {
221                        delta = -1;
222                }
223                matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i;
224            } else {
225                int cy = ((CellLayout.LayoutParams)
226                        hotseatParent.getChildAt(i).getLayoutParams()).cellY;
227                if ((includeAllappsicon && cy >= allappsiconRank) ||
228                        (!includeAllappsicon && cy > allappsiconRank)) {
229                        delta = -1;
230                }
231                matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i;
232            }
233        }
234        if (DEBUG) {
235            printMatrix(matrix);
236        }
237        return matrix;
238    }
239
240    /**
241     * Creates a sparse matrix that merges the icon of previous/next page and last column of
242     * current page. When left key is triggered on the leftmost column, sparse matrix is created
243     * that combines previous page matrix and an extra column on the right. Likewise, when right
244     * key is triggered on the rightmost column, sparse matrix is created that combines this column
245     * on the 0th column and the next page matrix.
246     *
247     * @param pivotX    x coordinate of the focused item in the current page
248     * @param pivotY    y coordinate of the focused item in the current page
249     */
250    // TODO: get rid of the dynamic matrix creation
251    public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) {
252
253        ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
254
255        int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY());
256
257        // Iterate thru the children of the top parent.
258        for (int i = 0; i < iconParent.getChildCount(); i++) {
259            int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX;
260            int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY;
261            if (pivotX < 0) {
262                matrix[cx - pivotX][cy] = i;
263            } else {
264                matrix[cx][cy] = i;
265            }
266        }
267
268        if (pivotX < 0) {
269            matrix[0][pivotY] = PIVOT;
270        } else {
271            matrix[pivotX][pivotY] = PIVOT;
272        }
273        if (DEBUG) {
274            printMatrix(matrix);
275        }
276        return matrix;
277    }
278
279    //
280    // key event handling methods.
281    //
282
283    /**
284     * Calculates icon that has is closest to the horizontal axis in reference to the cur icon.
285     *
286     * Example of the check order for KEYCODE_DPAD_RIGHT:
287     * [  ][  ][13][14][15]
288     * [  ][ 6][ 8][10][12]
289     * [ X][ 1][ 2][ 3][ 4]
290     * [  ][ 5][ 7][ 9][11]
291     */
292    // TODO: add unit tests to verify all permutation.
293    private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY,
294            int[][] matrix, int increment) {
295        if(matrix == null) {
296            throw new IllegalStateException("Dpad navigation requires a matrix.");
297        }
298        int newIconIndex = NOOP;
299
300        int xPos = -1;
301        int yPos = -1;
302        // Figure out the location of the icon.
303        for (int i = 0; i < cntX; i++) {
304            for (int j = 0; j < cntY; j++) {
305                if (matrix[i][j] == iconIdx) {
306                    xPos = i;
307                    yPos = j;
308                }
309            }
310        }
311        if (DEBUG) {
312            Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d",
313                    xPos, yPos, iconIdx));
314        }
315
316        // Rule1: check first in the horizontal direction
317        for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) {
318            if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) {
319                return newIconIndex;
320            }
321        }
322
323        // Rule2: check (x1-n, yPos + increment),   (x1-n, yPos - increment)
324        //              (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment)
325        int nextYPos1;
326        int nextYPos2;
327        int i = -1;
328        for (int coeff = 1; coeff < cntY; coeff++) {
329            nextYPos1 = yPos + coeff * increment;
330            nextYPos2 = yPos - coeff * increment;
331            for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) {
332                if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) {
333                    return newIconIndex;
334                }
335                if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) {
336                    return newIconIndex;
337                }
338            }
339        }
340        return newIconIndex;
341    }
342
343    /**
344     * Calculates icon that is closest to the vertical axis in reference to the current icon.
345     *
346     * Example of the check order for KEYCODE_DPAD_DOWN:
347     * [  ][  ][  ][ X][  ][  ][  ]
348     * [  ][  ][ 5][ 1][ 4][  ][  ]
349     * [  ][10][ 7][ 2][ 6][ 9][  ]
350     * [14][12][ 9][ 3][ 8][11][13]
351     */
352    // TODO: add unit tests to verify all permutation.
353    private static int handleDpadVertical(int iconIndex, int cntX, int cntY,
354            int [][] matrix, int increment) {
355        int newIconIndex = NOOP;
356        if(matrix == null) {
357            throw new IllegalStateException("Dpad navigation requires a matrix.");
358        }
359
360        int xPos = -1;
361        int yPos = -1;
362        // Figure out the location of the icon.
363        for (int i = 0; i< cntX; i++) {
364            for (int j = 0; j < cntY; j++) {
365                if (matrix[i][j] == iconIndex) {
366                    xPos = i;
367                    yPos = j;
368                }
369            }
370        }
371
372        if (DEBUG) {
373            Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d",
374                    xPos, yPos, iconIndex));
375        }
376
377        // Rule1: check first in the dpad direction
378        for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) {
379            if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) {
380                return newIconIndex;
381            }
382        }
383
384        // Rule2: check (xPos + increment, y_(1-n)),   (xPos - increment, y_(1-n))
385        //              (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n))
386        int nextXPos1;
387        int nextXPos2;
388        int j = -1;
389        for (int coeff = 1; coeff < cntX; coeff++) {
390            nextXPos1 = xPos + coeff * increment;
391            nextXPos2 = xPos - coeff * increment;
392            for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) {
393                if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) {
394                    return newIconIndex;
395                }
396                if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) {
397                    return newIconIndex;
398                }
399            }
400        }
401        return newIconIndex;
402    }
403
404    private static int handleMoveHome() {
405        return CURRENT_PAGE_FIRST_ITEM;
406    }
407
408    private static int handleMoveEnd() {
409        return CURRENT_PAGE_LAST_ITEM;
410    }
411
412    private static int handlePageDown(int pageIndex, int pageCount) {
413        if (pageIndex < pageCount -1) {
414            return NEXT_PAGE_FIRST_ITEM;
415        }
416        return CURRENT_PAGE_LAST_ITEM;
417    }
418
419    private static int handlePageUp(int pageIndex) {
420        if (pageIndex > 0) {
421            return PREVIOUS_PAGE_FIRST_ITEM;
422        } else {
423            return CURRENT_PAGE_FIRST_ITEM;
424        }
425    }
426
427    //
428    // Helper methods.
429    //
430
431    private static boolean isValid(int xPos, int yPos, int countX, int countY) {
432        return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY);
433    }
434
435    private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) {
436        int newIconIndex = NOOP;
437        if (isValid(x, y, cntX, cntY)) {
438            if (matrix[x][y] != -1) {
439                newIconIndex = matrix[x][y];
440                if (DEBUG) {
441                    Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d",
442                            x, y, matrix[x][y]));
443                }
444                return newIconIndex;
445            }
446        }
447        return newIconIndex;
448    }
449
450    /**
451     * Only used for debugging.
452     */
453    private static String getStringIndex(int index) {
454        switch(index) {
455            case NOOP: return "NOOP";
456            case PREVIOUS_PAGE_FIRST_ITEM:  return "PREVIOUS_PAGE_FIRST";
457            case PREVIOUS_PAGE_LAST_ITEM:   return "PREVIOUS_PAGE_LAST";
458            case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN";
459            case CURRENT_PAGE_FIRST_ITEM:   return "CURRENT_PAGE_FIRST";
460            case CURRENT_PAGE_LAST_ITEM:    return "CURRENT_PAGE_LAST";
461            case NEXT_PAGE_FIRST_ITEM:      return "NEXT_PAGE_FIRST";
462            case NEXT_PAGE_LEFT_COLUMN:     return "NEXT_PAGE_LEFT_COLUMN";
463            default:
464                return Integer.toString(index);
465        }
466    }
467
468    /**
469     * Only used for debugging.
470     */
471    private static void printMatrix(int[][] matrix) {
472        Log.v(TAG, "\tprintMap:");
473        int m = matrix.length;
474        int n = matrix[0].length;
475
476        for (int j=0; j < n; j++) {
477            String colY = "\t\t";
478            for (int i=0; i < m; i++) {
479                colY +=  String.format("%3d",matrix[i][j]);
480            }
481            Log.v(TAG, colY);
482        }
483    }
484
485    /**
486     * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or
487     * {@link #NEXT_PAGE_RIGHT_COLUMN}
488     * @return the view adjacent to {@param oldView} in the {@param nextPage}.
489     */
490    public static View getAdjacentChildInNextPage(
491            ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) {
492        final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY;
493
494        int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally()
495                ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1);
496
497        for (; column >= 0; column--) {
498            for (int row = newRow; row >= 0; row--) {
499                View newView = nextPage.getChildAt(column, row);
500                if (newView != null) {
501                    return newView;
502                }
503            }
504        }
505        return null;
506    }
507}
508