diff options
author | Hyunyoung Song <hyunyoungs@google.com> | 2015-07-23 21:24:37 -0700 |
---|---|---|
committer | Hyunyoung Song <hyunyoungs@google.com> | 2015-07-23 21:24:37 -0700 |
commit | 0c0d54637af7109ae22e21a117dba3efdaded4b5 (patch) | |
tree | fe9ed94353b09dda232fceb64691f96dcd5db8d3 /src/com/android/launcher3/util | |
parent | cf51139c6e1d57b57177109f2177edc41edf5bbc (diff) | |
parent | 13ef17a37e683b8ad5800e9f542b411180fbec2f (diff) | |
download | android_packages_apps_Trebuchet-0c0d54637af7109ae22e21a117dba3efdaded4b5.tar.gz android_packages_apps_Trebuchet-0c0d54637af7109ae22e21a117dba3efdaded4b5.tar.bz2 android_packages_apps_Trebuchet-0c0d54637af7109ae22e21a117dba3efdaded4b5.zip |
resolved conflicts for merge of 13ef17a3 to mnc-ub-dev
b/22609402
Change-Id: I80699d985088b9b79211284e710e8a6b8b90860b
Diffstat (limited to 'src/com/android/launcher3/util')
-rw-r--r-- | src/com/android/launcher3/util/ComponentKey.java | 81 | ||||
-rw-r--r-- | src/com/android/launcher3/util/CursorIconInfo.java | 70 | ||||
-rw-r--r-- | src/com/android/launcher3/util/FlingAnimation.java | 104 | ||||
-rw-r--r-- | src/com/android/launcher3/util/FocusLogic.java | 507 | ||||
-rw-r--r-- | src/com/android/launcher3/util/LauncherEdgeEffect.java | 365 | ||||
-rw-r--r-- | src/com/android/launcher3/util/LongArrayMap.java | 65 | ||||
-rw-r--r-- | src/com/android/launcher3/util/ManagedProfileHeuristic.java | 350 | ||||
-rw-r--r-- | src/com/android/launcher3/util/RevealOutlineProvider.java | 49 | ||||
-rw-r--r-- | src/com/android/launcher3/util/Thunk.java | 43 | ||||
-rw-r--r-- | src/com/android/launcher3/util/UiThreadCircularReveal.java | 57 | ||||
-rw-r--r-- | src/com/android/launcher3/util/WallpaperUtils.java | 123 |
11 files changed, 1814 insertions, 0 deletions
diff --git a/src/com/android/launcher3/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java new file mode 100644 index 000000000..6a7df4318 --- /dev/null +++ b/src/com/android/launcher3/util/ComponentKey.java @@ -0,0 +1,81 @@ +package com.android.launcher3.util; + +/** + * 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. + */ + +import android.content.ComponentName; +import android.content.Context; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.Arrays; + +public class ComponentKey { + + public final ComponentName componentName; + public final UserHandleCompat user; + + private final int mHashCode; + + public ComponentKey(ComponentName componentName, UserHandleCompat user) { + assert (componentName != null); + assert (user != null); + this.componentName = componentName; + this.user = user; + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + + } + + /** + * Creates a new component key from an encoded component key string in the form of + * [flattenedComponentString#userId]. If the userId is not present, then it defaults + * to the current user. + */ + public ComponentKey(Context context, String componentKeyStr) { + int userDelimiterIndex = componentKeyStr.indexOf("#"); + if (userDelimiterIndex != -1) { + String componentStr = componentKeyStr.substring(0, userDelimiterIndex); + Long componentUser = Long.valueOf(componentKeyStr.substring(userDelimiterIndex + 1)); + componentName = ComponentName.unflattenFromString(componentStr); + user = UserManagerCompat.getInstance(context) + .getUserForSerialNumber(componentUser.longValue()); + } else { + // No user provided, default to the current user + componentName = ComponentName.unflattenFromString(componentKeyStr); + user = UserHandleCompat.myUserHandle(); + } + mHashCode = Arrays.hashCode(new Object[] {componentName, user}); + } + + /** + * Encodes a component key as a string of the form [flattenedComponentString#userId]. + */ + public String flattenToString(Context context) { + return componentName.flattenToString() + "#" + + UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(Object o) { + ComponentKey other = (ComponentKey) o; + return other.componentName.equals(componentName) && other.user.equals(user); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/util/CursorIconInfo.java b/src/com/android/launcher3/util/CursorIconInfo.java new file mode 100644 index 000000000..cdf9e3c60 --- /dev/null +++ b/src/com/android/launcher3/util/CursorIconInfo.java @@ -0,0 +1,70 @@ +/* + * 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.util; + +import android.content.Context; +import android.content.Intent.ShortcutIconResource; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.text.TextUtils; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; + +/** + * Utility class to load icon from a cursor. + */ +public class CursorIconInfo { + public final int iconTypeIndex; + public final int iconPackageIndex; + public final int iconResourceIndex; + public final int iconIndex; + + public CursorIconInfo(Cursor c) { + iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); + iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); + iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); + iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); + } + + public Bitmap loadIcon(Cursor c, ShortcutInfo info, Context context) { + Bitmap icon = null; + int iconType = c.getInt(iconTypeIndex); + switch (iconType) { + case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: + String packageName = c.getString(iconPackageIndex); + String resourceName = c.getString(iconResourceIndex); + if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { + info.iconResource = new ShortcutIconResource(); + info.iconResource.packageName = packageName; + info.iconResource.resourceName = resourceName; + icon = Utilities.createIconBitmap(packageName, resourceName, context); + } + if (icon == null) { + // Failed to load from resource, try loading from DB. + icon = Utilities.createIconBitmap(c, iconIndex, context); + } + break; + case LauncherSettings.Favorites.ICON_TYPE_BITMAP: + icon = Utilities.createIconBitmap(c, iconIndex, context); + info.customIcon = icon != null; + break; + } + return icon; + } +} diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java new file mode 100644 index 000000000..55c5d7dc2 --- /dev/null +++ b/src/com/android/launcher3/util/FlingAnimation.java @@ -0,0 +1,104 @@ +package com.android.launcher3.util; + +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.animation.DecelerateInterpolator; + +import com.android.launcher3.DragLayer; +import com.android.launcher3.DragView; +import com.android.launcher3.DropTarget.DragObject; + +public class FlingAnimation implements AnimatorUpdateListener { + + /** + * Maximum acceleration in one dimension (pixels per milliseconds) + */ + private static final float MAX_ACCELERATION = 0.5f; + private static final int DRAG_END_DELAY = 300; + + protected final DragObject mDragObject; + protected final Rect mIconRect; + protected final DragLayer mDragLayer; + protected final Rect mFrom; + protected final int mDuration; + protected final float mUX, mUY; + protected final float mAnimationTimeFraction; + protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); + + protected float mAX, mAY; + + /** + * @param vel initial fling velocity in pixels per second. + */ + public FlingAnimation(DragObject d, PointF vel, Rect iconRect, DragLayer dragLayer) { + mDragObject = d; + mUX = vel.x / 1000; + mUY = vel.y / 1000; + mIconRect = iconRect; + + mDragLayer = dragLayer; + mFrom = new Rect(); + dragLayer.getViewRectRelativeToSelf(d.dragView, mFrom); + + float scale = d.dragView.getScaleX(); + float xOffset = ((scale - 1f) * d.dragView.getMeasuredWidth()) / 2f; + float yOffset = ((scale - 1f) * d.dragView.getMeasuredHeight()) / 2f; + mFrom.left += xOffset; + mFrom.right -= xOffset; + mFrom.top += yOffset; + mFrom.bottom -= yOffset; + + mDuration = initDuration(); + mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY); + } + + /** + * The fling animation is based on the following system + * - Apply a constant force in the y direction to causing the fling to decelerate. + * - The animation runs for the time taken by the object to go out of the screen. + * - Calculate a constant acceleration in x direction such that the object reaches + * {@link #mIconRect} in the given time. + */ + protected int initDuration() { + float sY = -mFrom.bottom; + + float d = mUY * mUY + 2 * sY * MAX_ACCELERATION; + if (d >= 0) { + // sY can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for y direction. + mAY = MAX_ACCELERATION; + } else { + // sY is not reachable, decrease the acceleration so that sY is almost reached. + d = 0; + mAY = mUY * mUY / (2 * -sY); + } + double t = (-mUY - Math.sqrt(d)) / mAY; + + float sX = -mFrom.exactCenterX() + mIconRect.exactCenterX(); + + // Find horizontal acceleration such that: u*t + a*t*t/2 = s + mAX = (float) ((sX - t * mUX) * 2 / (t * t)); + return (int) Math.round(t); + } + + public final int getDuration() { + return mDuration + DRAG_END_DELAY; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = animation.getAnimatedFraction(); + if (t > mAnimationTimeFraction) { + t = 1; + } else { + t = t / mAnimationTimeFraction; + } + final DragView dragView = (DragView) mDragLayer.getAnimatedView(); + final float time = t * mDuration; + dragView.setTranslationX(time * mUX + mFrom.left + mAX * time * time / 2); + dragView.setTranslationY(time * mUY + mFrom.top + mAY * time * time / 2); + dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + } +} diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java new file mode 100644 index 000000000..696eabe00 --- /dev/null +++ b/src/com/android/launcher3/util/FocusLogic.java @@ -0,0 +1,507 @@ +/* + * 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.util; + +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; + +import com.android.launcher3.CellLayout; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.ShortcutAndWidgetContainer; + +import java.util.Arrays; + +/** + * Calculates the next item that a {@link KeyEvent} should change the focus to. + *<p> + * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates. + * Currently supports: + * <ul> + * <li> full matrix of cells that are 1x1 + * <li> sparse matrix of cells that are 1x1 + * [ 1][ ][ 2][ ] + * [ ][ ][ 3][ ] + * [ ][ 4][ ][ ] + * [ ][ 5][ 6][ 7] + * </ul> + * *<p> + * For testing, one can use a BT keyboard, or use following adb command. + * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT + */ +public class FocusLogic { + + private static final String TAG = "FocusLogic"; + private static final boolean DEBUG = false; + + // Item and page index related constant used by {@link #handleKeyEvent}. + public static final int NOOP = -1; + + public static final int PREVIOUS_PAGE_RIGHT_COLUMN = -2; + public static final int PREVIOUS_PAGE_FIRST_ITEM = -3; + public static final int PREVIOUS_PAGE_LAST_ITEM = -4; + public static final int PREVIOUS_PAGE_LEFT_COLUMN = -5; + + public static final int CURRENT_PAGE_FIRST_ITEM = -6; + public static final int CURRENT_PAGE_LAST_ITEM = -7; + + public static final int NEXT_PAGE_FIRST_ITEM = -8; + public static final int NEXT_PAGE_LEFT_COLUMN = -9; + public static final int NEXT_PAGE_RIGHT_COLUMN = -10; + + // Matrix related constant. + public static final int EMPTY = -1; + public static final int PIVOT = 100; + + /** + * Returns true only if this utility class handles the key code. + */ + public static boolean shouldConsume(int keyCode) { + return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || + keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || + keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END || + keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || + keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL); + } + + public static int handleKeyEvent(int keyCode, int cntX, int cntY, + int [][] map, int iconIdx, int pageIndex, int pageCount, boolean isRtl) { + + if (DEBUG) { + Log.v(TAG, String.format( + "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d", + cntX, cntY, iconIdx, pageIndex, pageCount)); + } + + int newIndex = NOOP; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_RIGHT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_RIGHT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/); + if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) { + newIndex = NEXT_PAGE_LEFT_COLUMN; + } else if (isRtl && newIndex == NOOP && pageIndex > 0) { + newIndex = PREVIOUS_PAGE_LEFT_COLUMN; + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1 /*increment*/); + break; + case KeyEvent.KEYCODE_DPAD_UP: + newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1 /*increment*/); + break; + case KeyEvent.KEYCODE_MOVE_HOME: + newIndex = handleMoveHome(); + break; + case KeyEvent.KEYCODE_MOVE_END: + newIndex = handleMoveEnd(); + break; + case KeyEvent.KEYCODE_PAGE_DOWN: + newIndex = handlePageDown(pageIndex, pageCount); + break; + case KeyEvent.KEYCODE_PAGE_UP: + newIndex = handlePageUp(pageIndex); + break; + default: + break; + } + + if (DEBUG) { + Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]", + iconIdx, getStringIndex(newIndex))); + } + return newIndex; + } + + /** + * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}. + * + * @param m number of columns in the matrix + * @param n number of rows in the matrix + */ + // TODO: get rid of dynamic matrix creation. + private static int[][] createFullMatrix(int m, int n) { + int[][] matrix = new int [m][n]; + + for (int i=0; i < m;i++) { + Arrays.fill(matrix[i], EMPTY); + } + return matrix; + } + + /** + * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the + * index of the child view. + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout layout) { + ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets(); + final int m = layout.getCountX(); + final int n = layout.getCountY(); + final boolean invert = parent.invertLayoutHorizontally(); + + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children. + for (int i = 0; i < parent.getChildCount(); i++ ) { + int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY; + matrix[invert ? (m - cx - 1) : cx][cy] = i; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout. + * The size of the returning matrix is [icon column count x (icon + hotseat row count)] + * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)] + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout, + boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets(); + + int m, n; + if (isHorizontal) { + m = iconLayout.getCountX(); + n = iconLayout.getCountY() + hotseatLayout.getCountY(); + } else { + m = iconLayout.getCountX() + hotseatLayout.getCountX(); + n = iconLayout.getCountY(); + } + int[][] matrix = createFullMatrix(m, n); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + matrix[cx][cy] = i; + } + + // Iterate thru the children of the bottom parent + // The hotseat view group contains one more item than iconLayout column count. + // If {@param allappsiconRank} not negative, then the last icon in the hotseat + // is truncated. If it is negative, then all apps icon index is not inserted. + for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) { + int delta = 0; + if (isHorizontal) { + int cx = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellX; + if ((includeAllappsicon && cx >= allappsiconRank) || + (!includeAllappsicon && cx > allappsiconRank)) { + delta = -1; + } + matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i; + } else { + int cy = ((CellLayout.LayoutParams) + hotseatParent.getChildAt(i).getLayoutParams()).cellY; + if ((includeAllappsicon && cy >= allappsiconRank) || + (!includeAllappsicon && cy > allappsiconRank)) { + delta = -1; + } + matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i; + } + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + /** + * Creates a sparse matrix that merges the icon of previous/next page and last column of + * current page. When left key is triggered on the leftmost column, sparse matrix is created + * that combines previous page matrix and an extra column on the right. Likewise, when right + * key is triggered on the rightmost column, sparse matrix is created that combines this column + * on the 0th column and the next page matrix. + * + * @param pivotX x coordinate of the focused item in the current page + * @param pivotY y coordinate of the focused item in the current page + */ + // TODO: get rid of the dynamic matrix creation + public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) { + + ViewGroup iconParent = iconLayout.getShortcutsAndWidgets(); + + int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY()); + + // Iterate thru the children of the top parent. + for (int i = 0; i < iconParent.getChildCount(); i++) { + int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX; + int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY; + if (pivotX < 0) { + matrix[cx - pivotX][cy] = i; + } else { + matrix[cx][cy] = i; + } + } + + if (pivotX < 0) { + matrix[0][pivotY] = PIVOT; + } else { + matrix[pivotX][pivotY] = PIVOT; + } + if (DEBUG) { + printMatrix(matrix); + } + return matrix; + } + + // + // key event handling methods. + // + + /** + * Calculates icon that has is closest to the horizontal axis in reference to the cur icon. + * + * Example of the check order for KEYCODE_DPAD_RIGHT: + * [ ][ ][13][14][15] + * [ ][ 6][ 8][10][12] + * [ X][ 1][ 2][ 3][ 4] + * [ ][ 5][ 7][ 9][11] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY, + int[][] matrix, int increment) { + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + int newIconIndex = NOOP; + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i < cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIdx) { + xPos = i; + yPos = j; + } + } + } + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIdx)); + } + + // Rule1: check first in the horizontal direction + for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (x1-n, yPos + increment), (x1-n, yPos - increment) + // (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment) + int nextYPos1; + int nextYPos2; + int i = -1; + for (int coeff = 1; coeff < cntY; coeff++) { + nextYPos1 = yPos + coeff * increment; + nextYPos2 = yPos - coeff * increment; + for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) { + if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + /** + * Calculates icon that is closest to the vertical axis in reference to the current icon. + * + * Example of the check order for KEYCODE_DPAD_DOWN: + * [ ][ ][ ][ X][ ][ ][ ] + * [ ][ ][ 5][ 1][ 4][ ][ ] + * [ ][10][ 7][ 2][ 6][ 9][ ] + * [14][12][ 9][ 3][ 8][11][13] + */ + // TODO: add unit tests to verify all permutation. + private static int handleDpadVertical(int iconIndex, int cntX, int cntY, + int [][] matrix, int increment) { + int newIconIndex = NOOP; + if(matrix == null) { + throw new IllegalStateException("Dpad navigation requires a matrix."); + } + + int xPos = -1; + int yPos = -1; + // Figure out the location of the icon. + for (int i = 0; i< cntX; i++) { + for (int j = 0; j < cntY; j++) { + if (matrix[i][j] == iconIndex) { + xPos = i; + yPos = j; + } + } + } + + if (DEBUG) { + Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d", + xPos, yPos, iconIndex)); + } + + // Rule1: check first in the dpad direction + for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) { + if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + + // Rule2: check (xPos + increment, y_(1-n)), (xPos - increment, y_(1-n)) + // (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n)) + int nextXPos1; + int nextXPos2; + int j = -1; + for (int coeff = 1; coeff < cntX; coeff++) { + nextXPos1 = xPos + coeff * increment; + nextXPos2 = xPos - coeff * increment; + for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) { + if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) { + return newIconIndex; + } + } + } + return newIconIndex; + } + + private static int handleMoveHome() { + return CURRENT_PAGE_FIRST_ITEM; + } + + private static int handleMoveEnd() { + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageDown(int pageIndex, int pageCount) { + if (pageIndex < pageCount -1) { + return NEXT_PAGE_FIRST_ITEM; + } + return CURRENT_PAGE_LAST_ITEM; + } + + private static int handlePageUp(int pageIndex) { + if (pageIndex > 0) { + return PREVIOUS_PAGE_FIRST_ITEM; + } else { + return CURRENT_PAGE_FIRST_ITEM; + } + } + + // + // Helper methods. + // + + private static boolean isValid(int xPos, int yPos, int countX, int countY) { + return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY); + } + + private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) { + int newIconIndex = NOOP; + if (isValid(x, y, cntX, cntY)) { + if (matrix[x][y] != -1) { + newIconIndex = matrix[x][y]; + if (DEBUG) { + Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d", + x, y, matrix[x][y])); + } + return newIconIndex; + } + } + return newIconIndex; + } + + /** + * Only used for debugging. + */ + private static String getStringIndex(int index) { + switch(index) { + case NOOP: return "NOOP"; + case PREVIOUS_PAGE_FIRST_ITEM: return "PREVIOUS_PAGE_FIRST"; + case PREVIOUS_PAGE_LAST_ITEM: return "PREVIOUS_PAGE_LAST"; + case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN"; + case CURRENT_PAGE_FIRST_ITEM: return "CURRENT_PAGE_FIRST"; + case CURRENT_PAGE_LAST_ITEM: return "CURRENT_PAGE_LAST"; + case NEXT_PAGE_FIRST_ITEM: return "NEXT_PAGE_FIRST"; + case NEXT_PAGE_LEFT_COLUMN: return "NEXT_PAGE_LEFT_COLUMN"; + default: + return Integer.toString(index); + } + } + + /** + * Only used for debugging. + */ + private static void printMatrix(int[][] matrix) { + Log.v(TAG, "\tprintMap:"); + int m = matrix.length; + int n = matrix[0].length; + + for (int j=0; j < n; j++) { + String colY = "\t\t"; + for (int i=0; i < m; i++) { + colY += String.format("%3d",matrix[i][j]); + } + Log.v(TAG, colY); + } + } + + /** + * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or + * {@link #NEXT_PAGE_RIGHT_COLUMN} + * @return the view adjacent to {@param oldView} in the {@param nextPage}. + */ + public static View getAdjacentChildInNextPage( + ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) { + final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY; + + int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally() + ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1); + + for (; column >= 0; column--) { + for (int row = newRow; row >= 0; row--) { + View newView = nextPage.getChildAt(column, row); + if (newView != null) { + return newView; + } + } + } + return null; + } +} diff --git a/src/com/android/launcher3/util/LauncherEdgeEffect.java b/src/com/android/launcher3/util/LauncherEdgeEffect.java new file mode 100644 index 000000000..3e3b255ad --- /dev/null +++ b/src/com/android/launcher3/util/LauncherEdgeEffect.java @@ -0,0 +1,365 @@ +/* + * 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.util; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +/** + * This class differs from the framework {@link android.widget.EdgeEffect}: + * 1) It does not use PorterDuffXfermode + * 2) The width to radius factor is smaller (0.5 instead of 0.75) + */ +public class LauncherEdgeEffect { + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 600; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 2000; + + private static final float MAX_ALPHA = 0.5f; + + private static final float MAX_GLOW_SCALE = 2.f; + + private static final float PULL_GLOW_BEGIN = 0.f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + // Maximum velocity, clamps at this value + private static final int MAX_VELOCITY = 10000; + + private static final float EPSILON = 0.001f; + + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + + private float mGlowAlpha; + private float mGlowScaleY; + + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f; + + private static final int VELOCITY_GLOW_FACTOR = 6; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + private final Rect mBounds = new Rect(); + private final Paint mPaint = new Paint(); + private float mRadius; + private float mBaseGlowScale; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + */ + public LauncherEdgeEffect() { + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + final float r = width * 0.5f / SIN; + final float y = COS * r; + final float h = r - y; + final float or = height * 0.75f / SIN; + final float oy = COS * or; + final float oh = or - oy; + + mRadius = r; + mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f; + + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.</p> + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + + final float absdd = Math.abs(deltaDistance); + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + if (mPullDistance == 0) { + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = (float) (Math.max(0, 1 - 1 / + Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d); + + mGlowScaleY = mGlowScaleYStart = scale; + } + + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method <code>getCurrVelocity</code> will provide a reasonable approximation + * to use here.</p> + * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.15f + (velocity * 0.02f); + + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.3f; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); + + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; + } + + /** + * Set the color of this edge effect in argb. + * + * @param color Color in argb + */ + public void setColor(int color) { + mPaint.setColor(color); + } + + /** + * Return the color of this edge effect in argb. + * @return The color of this edge effect in argb + */ + public int getColor() { + return mPaint.getColor(); + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(Canvas canvas) { + update(); + + final float centerX = mBounds.centerX(); + final float centerY = mBounds.height() - mRadius; + + canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0); + + final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; + float translateX = mBounds.width() * displacement / 2; + mPaint.setAlpha((int) (0xff * mGlowAlpha)); + canvas.drawCircle(centerX + translateX, centerY, mRadius, mPaint); + + boolean oneLastFrame = false; + if (mState == STATE_RECEDE && mGlowScaleY == 0) { + mState = STATE_IDLE; + oneLastFrame = true; + } + + return mState != STATE_IDLE || oneLastFrame; + } + + /** + * Return the maximum height that the edge effect will be drawn at given the original + * {@link #setSize(int, int) input size}. + * @return The maximum height of the edge effect + */ + public int getMaxHeight() { + return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f); + } + + private void update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + mDisplacement = (mDisplacement + mTargetDisplacement) / 2; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = RECEDE_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = PULL_DECAY_TIME; + + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow should fade to nothing. + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } +} diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/LongArrayMap.java new file mode 100644 index 000000000..a337e85bd --- /dev/null +++ b/src/com/android/launcher3/util/LongArrayMap.java @@ -0,0 +1,65 @@ +/* + * 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.util; + +import android.util.LongSparseArray; + +import java.util.Iterator; + +/** + * Extension of {@link LongSparseArray} with some utility methods. + */ +public class LongArrayMap<E> extends LongSparseArray<E> implements Iterable<E> { + + public boolean containsKey(long key) { + return indexOfKey(key) >= 0; + } + + public boolean isEmpty() { + return size() <= 0; + } + + @Override + public LongArrayMap<E> clone() { + return (LongArrayMap<E>) super.clone(); + } + + @Override + public Iterator<E> iterator() { + return new ValueIterator(); + } + + @Thunk class ValueIterator implements Iterator<E> { + + private int mNextIndex = 0; + + @Override + public boolean hasNext() { + return mNextIndex < size(); + } + + @Override + public E next() { + return valueAt(mNextIndex ++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java new file mode 100644 index 000000000..b37f44719 --- /dev/null +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -0,0 +1,350 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.util.Log; + +import com.android.launcher3.FolderInfo; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.LauncherActivityInfoCompat; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.compat.UserManagerCompat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Handles addition of app shortcuts for managed profiles. + * Methods of class should only be called on {@link LauncherModel#sWorkerThread}. + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class ManagedProfileHeuristic { + + private static final String TAG = "ManagedProfileHeuristic"; + + /** + * Maintain a set of packages installed per user. + */ + private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_"; + + private static final String USER_FOLDER_ID_PREFIX = "user_folder_"; + + /** + * Duration (in milliseconds) for which app shortcuts will be added to work folder. + */ + private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000; + + public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) { + if (Utilities.isLmpOrAbove() && !UserHandleCompat.myUserHandle().equals(user)) { + return new ManagedProfileHeuristic(context, user); + } + return null; + } + + private final Context mContext; + private final UserHandleCompat mUser; + private final LauncherModel mModel; + + private final SharedPreferences mPrefs; + private final long mUserSerial; + private final long mUserCreationTime; + private final String mPackageSetKey; + + private ArrayList<ShortcutInfo> mHomescreenApps; + private ArrayList<ShortcutInfo> mWorkFolderApps; + + private ManagedProfileHeuristic(Context context, UserHandleCompat user) { + mContext = context; + mUser = user; + mModel = LauncherAppState.getInstance().getModel(); + + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + mUserSerial = userManager.getSerialNumberForUser(user); + mUserCreationTime = userManager.getUserCreationTime(user); + mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial; + + mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + + /** + * Checks the list of user apps and adds icons for newly installed apps on the homescreen or + * workfolder. + */ + public void processUserApps(List<LauncherActivityInfoCompat> apps) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet<String> packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + + for (LauncherActivityInfoCompat info : apps) { + String packageName = info.getComponentName().getPackageName(); + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + try { + PackageInfo pkgInfo = mContext.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); + markForAddition(info, pkgInfo.firstInstallTime); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unknown package " + packageName, e); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + // Do not add shortcuts on the homescreen for the first time. This prevents the launcher + // getting filled with the managed user apps, when it start with a fresh DB (or after + // a very long time). + finalizeAdditions(userAppsExisted); + } + } + + private void markForAddition(LauncherActivityInfoCompat info, long installTime) { + ArrayList<ShortcutInfo> targetList = + (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ? + mWorkFolderApps : mHomescreenApps; + targetList.add(ShortcutInfo.fromActivityInfo(info, mContext)); + } + + /** + * Adds and binds shortcuts marked to be added to the work folder. + */ + private void finalizeWorkFolder() { + if (mWorkFolderApps.isEmpty()) { + return; + } + Collections.sort(mWorkFolderApps, new Comparator<ShortcutInfo>() { + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + return Long.compare(lhs.firstInstallTime, rhs.firstInstallTime); + } + }); + + // Try to get a work folder. + String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial; + if (mPrefs.contains(folderIdKey)) { + long folderId = mPrefs.getLong(folderIdKey, 0); + final FolderInfo workFolder = mModel.findFolderById(folderId); + + if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) { + // Could not get a work folder. Add all the icons to homescreen. + mHomescreenApps.addAll(mWorkFolderApps); + return; + } + saveWorkFolderShortcuts(folderId, workFolder.contents.size()); + + final ArrayList<ShortcutInfo> shortcuts = mWorkFolderApps; + // FolderInfo could already be bound. We need to add shortcuts on the UI thread. + new MainThreadExecutor().execute(new Runnable() { + + @Override + public void run() { + for (ShortcutInfo info : shortcuts) { + workFolder.add(info); + } + } + }); + } else { + // Create a new folder. + final FolderInfo workFolder = new FolderInfo(); + workFolder.title = mContext.getText(R.string.work_folder_name); + workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null); + + // Add all shortcuts before adding it to the UI, as an empty folder might get deleted. + for (ShortcutInfo info : mWorkFolderApps) { + workFolder.add(info); + } + + // Add the item to home screen and DB. This also generates an item id synchronously. + ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1); + itemList.add(workFolder); + mModel.addAndBindAddedWorkspaceItems(mContext, itemList); + mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply(); + + saveWorkFolderShortcuts(workFolder.id, 0); + } + } + + /** + * Add work folder shortcuts to the DB. + */ + private void saveWorkFolderShortcuts(long workFolderId, int startingRank) { + for (ItemInfo info : mWorkFolderApps) { + info.rank = startingRank++; + LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0); + } + } + + /** + * Adds and binds all shortcuts marked for addition. + */ + private void finalizeAdditions(boolean addHomeScreenShortcuts) { + finalizeWorkFolder(); + + if (addHomeScreenShortcuts && !mHomescreenApps.isEmpty()) { + mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps); + } + } + + /** + * Updates the list of installed apps and adds any new icons on homescreen or work folder. + */ + public void processPackageAdd(String[] packages) { + mHomescreenApps = new ArrayList<>(); + mWorkFolderApps = new ArrayList<>(); + + HashSet<String> packageSet = new HashSet<>(); + final boolean userAppsExisted = getUserApps(packageSet); + + boolean newPackageAdded = false; + long installTime = System.currentTimeMillis(); + LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext); + + for (String packageName : packages) { + if (!packageSet.contains(packageName)) { + packageSet.add(packageName); + newPackageAdded = true; + + List<LauncherActivityInfoCompat> activities = + launcherApps.getActivityList(packageName, mUser); + if (!activities.isEmpty()) { + markForAddition(activities.get(0), installTime); + } + } + } + + if (newPackageAdded) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + finalizeAdditions(userAppsExisted); + } + } + + /** + * Updates the list of installed packages for the user. + */ + public void processPackageRemoved(String[] packages) { + HashSet<String> packageSet = new HashSet<String>(); + getUserApps(packageSet); + boolean packageRemoved = false; + + for (String packageName : packages) { + if (packageSet.remove(packageName)) { + packageRemoved = true; + } + } + + if (packageRemoved) { + mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply(); + } + } + + /** + * Reads the list of user apps which have already been processed. + * @return false if the list didn't exist, true otherwise + */ + private boolean getUserApps(HashSet<String> outExistingApps) { + Set<String> userApps = mPrefs.getStringSet(mPackageSetKey, null); + if (userApps == null) { + return false; + } else { + outExistingApps.addAll(userApps); + return true; + } + } + + /** + * Verifies that entries corresponding to {@param users} exist and removes all invalid entries. + */ + public static void processAllUsers(List<UserHandleCompat> users, Context context) { + if (!Utilities.isLmpOrAbove()) { + return; + } + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + HashSet<String> validKeys = new HashSet<String>(); + for (UserHandleCompat user : users) { + addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys); + } + + SharedPreferences prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + for (String key : prefs.getAll().keySet()) { + if (!validKeys.contains(key)) { + editor.remove(key); + } + } + editor.apply(); + } + + private static void addAllUserKeys(long userSerial, HashSet<String> keysOut) { + keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial); + keysOut.add(USER_FOLDER_ID_PREFIX + userSerial); + } + + /** + * For each user, if a work folder has not been created, mark it such that the folder will + * never get created. + */ + public static void markExistingUsersForNoFolderCreation(Context context) { + UserManagerCompat userManager = UserManagerCompat.getInstance(context); + UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + + SharedPreferences prefs = null; + for (UserHandleCompat user : userManager.getUserProfiles()) { + if (myUser.equals(user)) { + continue; + } + + if (prefs == null) { + prefs = context.getSharedPreferences( + LauncherFiles.MANAGED_USER_PREFERENCES_KEY, + Context.MODE_PRIVATE); + } + String folderIdKey = USER_FOLDER_ID_PREFIX + userManager.getSerialNumberForUser(user); + if (!prefs.contains(folderIdKey)) { + prefs.edit().putLong(folderIdKey, ItemInfo.NO_ID).apply(); + } + } + } +} diff --git a/src/com/android/launcher3/util/RevealOutlineProvider.java b/src/com/android/launcher3/util/RevealOutlineProvider.java new file mode 100644 index 000000000..0db3984f8 --- /dev/null +++ b/src/com/android/launcher3/util/RevealOutlineProvider.java @@ -0,0 +1,49 @@ +package com.android.launcher3.util; + +import android.annotation.TargetApi; +import android.graphics.Outline; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RevealOutlineProvider extends ViewOutlineProvider { + + private int mCenterX; + private int mCenterY; + private float mRadius0; + private float mRadius1; + private int mCurrentRadius; + + private final Rect mOval; + + /** + * @param x reveal center x + * @param y reveal center y + * @param r0 initial radius + * @param r1 final radius + */ + public RevealOutlineProvider(int x, int y, float r0, float r1) { + mCenterX = x; + mCenterY = y; + mRadius0 = r0; + mRadius1 = r1; + + mOval = new Rect(); + } + + public void setProgress(float progress) { + mCurrentRadius = (int) ((1 - progress) * mRadius0 + progress * mRadius1); + + mOval.left = mCenterX - mCurrentRadius; + mOval.top = mCenterY - mCurrentRadius; + mOval.right = mCenterX + mCurrentRadius; + mOval.bottom = mCenterY + mCurrentRadius; + } + + @Override + public void getOutline(View v, Outline outline) { + outline.setRoundRect(mOval, mCurrentRadius); + } +} diff --git a/src/com/android/launcher3/util/Thunk.java b/src/com/android/launcher3/util/Thunk.java new file mode 100644 index 000000000..de350b068 --- /dev/null +++ b/src/com/android/launcher3/util/Thunk.java @@ -0,0 +1,43 @@ +/* + * 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.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the given field or method has package visibility solely to prevent the creation + * of a synthetic method. In practice, you should treat this field/method as if it were private. + * <p> + * + * When a private method is called from an inner class, the Java compiler generates a simple + * package private shim method that the class generated from the inner class can call. This results + * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method + * count limit. + * <p> + * + * If you'd like to see warnings for these synthetic methods in eclipse, turn on: + * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member + * of an enclosing type". + * <p> + * + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface Thunk { }
\ No newline at end of file diff --git a/src/com/android/launcher3/util/UiThreadCircularReveal.java b/src/com/android/launcher3/util/UiThreadCircularReveal.java new file mode 100644 index 000000000..3ca3aeeee --- /dev/null +++ b/src/com/android/launcher3/util/UiThreadCircularReveal.java @@ -0,0 +1,57 @@ +package com.android.launcher3.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.android.launcher3.Utilities; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class UiThreadCircularReveal { + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1) { + return createCircularReveal(v, x, y, r0, r1, ViewOutlineProvider.BACKGROUND); + } + + public static ValueAnimator createCircularReveal(View v, int x, int y, float r0, float r1, + final ViewOutlineProvider originalProvider) { + ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + + final View revealView = v; + final RevealOutlineProvider outlineProvider = new RevealOutlineProvider(x, y, r0, r1); + final float elevation = v.getElevation(); + + va.addListener(new AnimatorListenerAdapter() { + public void onAnimationStart(Animator animation) { + revealView.setOutlineProvider(outlineProvider); + revealView.setClipToOutline(true); + revealView.setTranslationZ(-elevation); + } + + public void onAnimationEnd(Animator animation) { + revealView.setOutlineProvider(originalProvider); + revealView.setClipToOutline(false); + revealView.setTranslationZ(0); + } + + }); + + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator arg0) { + float progress = arg0.getAnimatedFraction(); + outlineProvider.setProgress(progress); + revealView.invalidateOutline(); + if (!Utilities.isLmpMR1OrAbove()) { + revealView.invalidate(); + } + } + }); + return va; + } +} diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java new file mode 100644 index 000000000..53b2acd84 --- /dev/null +++ b/src/com/android/launcher3/util/WallpaperUtils.java @@ -0,0 +1,123 @@ +/* + * 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.util; + +import android.annotation.TargetApi; +import android.app.WallpaperManager; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Point; +import android.os.Build; +import android.view.WindowManager; + +/** + * Utility methods for wallpaper management. + */ +public final class WallpaperUtils { + + public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + public static final float WALLPAPER_SCREENS_SPAN = 2f; + + public static void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager, boolean fallBackToDefaults) { + final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1); + + if (savedWidth == -1 || savedHeight == -1) { + if (!fallBackToDefaults) { + return; + } else { + savedWidth = defaultWallpaperSize.x; + savedHeight = defaultWallpaperSize.y; + } + } + + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + /** + * As a ratio of screen height, the total distance we want the parallax effect to span + * horizontally + */ + public static float wallpaperTravelToScreenWidthRatio(int width, int height) { + float aspectRatio = width / (float) height; + + // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width + // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width + // We will use these two data points to extrapolate how much the wallpaper parallax effect + // to span (ie travel) at any aspect ratio: + + final float ASPECT_RATIO_LANDSCAPE = 16/10f; + final float ASPECT_RATIO_PORTRAIT = 10/16f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f; + final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f; + + // To find out the desired width at different aspect ratios, we use the following two + // formulas, where the coefficient on x is the aspect ratio (width/height): + // (16/10)x + y = 1.5 + // (10/16)x + y = 1.2 + // We solve for x and y and end up with a final formula: + final float x = + (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) / + (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT); + final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT; + return x * aspectRatio + y; + } + + private static Point sDefaultWallpaperSize; + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) { + if (sDefaultWallpaperSize == null) { + Point minDims = new Point(); + Point maxDims = new Point(); + windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims); + + int maxDim = Math.max(maxDims.x, maxDims.y); + int minDim = Math.max(minDims.x, minDims.y); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + Point realSize = new Point(); + windowManager.getDefaultDisplay().getRealSize(realSize); + maxDim = Math.max(realSize.x, realSize.y); + minDim = Math.min(realSize.x, realSize.y); + } + + // We need to ensure that there is enough extra space in the wallpaper + // for the intended parallax effects + final int defaultWidth, defaultHeight; + if (res.getConfiguration().smallestScreenWidthDp >= 720) { + defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim)); + defaultHeight = maxDim; + } else { + defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim); + defaultHeight = maxDim; + } + sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight); + } + return sDefaultWallpaperSize; + } +} |