summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/util')
-rw-r--r--src/com/android/launcher3/util/ComponentKey.java29
-rw-r--r--src/com/android/launcher3/util/CursorIconInfo.java70
-rw-r--r--src/com/android/launcher3/util/FlingAnimation.java104
-rw-r--r--src/com/android/launcher3/util/FocusLogic.java507
-rw-r--r--src/com/android/launcher3/util/LauncherEdgeEffect.java365
-rw-r--r--src/com/android/launcher3/util/LongArrayMap.java65
-rw-r--r--src/com/android/launcher3/util/ManagedProfileHeuristic.java350
-rw-r--r--src/com/android/launcher3/util/RevealOutlineProvider.java49
-rw-r--r--src/com/android/launcher3/util/Thunk.java43
-rw-r--r--src/com/android/launcher3/util/UiThreadCircularReveal.java55
-rw-r--r--src/com/android/launcher3/util/WallpaperUtils.java123
11 files changed, 1758 insertions, 2 deletions
diff --git a/src/com/android/launcher3/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java
index 7a7ed4a4f..6a7df4318 100644
--- a/src/com/android/launcher3/util/ComponentKey.java
+++ b/src/com/android/launcher3/util/ComponentKey.java
@@ -19,6 +19,7 @@ package com.android.launcher3.util;
import android.content.ComponentName;
import android.content.Context;
import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
import java.util.Arrays;
@@ -38,9 +39,33 @@ public class ComponentKey {
}
+ /**
+ * 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) {
- // Do nothing
- throw new UnsupportedOperationException();
+ 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
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..c7324fb1b
--- /dev/null
+++ b/src/com/android/launcher3/util/UiThreadCircularReveal.java
@@ -0,0 +1,55 @@
+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) {
+ ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+
+ final View revealView = v;
+ final RevealOutlineProvider outlineProvider = new RevealOutlineProvider(x, y, r0, r1);
+ final ViewOutlineProvider originalProvider = revealView.getOutlineProvider();
+ 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);
+ if (Utilities.isLmpMR1OrAbove()) {
+ revealView.invalidateOutline();
+ } else {
+ // On L, a bug requires calling a full view invalidate.
+ 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;
+ }
+}