diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2015-09-10 16:34:09 -0700 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2015-09-10 17:22:17 -0700 |
commit | ea9ad5cead9ad894fb670476bb5381198cdcf2de (patch) | |
tree | fcf23a25aadc660bd2dbb8b5178da454e8a00f08 /src/com/android/launcher3 | |
parent | 5845d3dbea53d513466c98b301eb49e96fe5d6a3 (diff) | |
parent | 4abaf133546b0c950edc82594985e9da50d9c1dd (diff) | |
download | android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.tar.gz android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.tar.bz2 android_packages_apps_Trebuchet-ea9ad5cead9ad894fb670476bb5381198cdcf2de.zip |
Merge remote-tracking branch 'origin/ub-launcher3-burnaby' into mnc-dev
Conflicts:
Android.mk
Change-Id: I05429e418a25b94e7669e002d39bc70806396b8e
Diffstat (limited to 'src/com/android/launcher3')
74 files changed, 2789 insertions, 1170 deletions
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index ea7c22189..e6bf52531 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -75,8 +75,8 @@ public class AppWidgetResizeFrame extends FrameLayout { mResizeMode = info.resizeMode; mDragLayer = dragLayer; - mMinHSpan = info.getMinSpanX(mLauncher); - mMinVSpan = info.getMinSpanY(mLauncher); + mMinHSpan = info.minSpanX; + mMinVSpan = info.minSpanY; setBackgroundResource(R.drawable.widget_resize_shadow); setForeground(getResources().getDrawable(R.drawable.widget_resize_frame)); diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index 5e7a012d2..b1d51ece0 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -13,6 +13,7 @@ import android.os.AsyncTask; import android.util.Log; import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.compat.AppWidgetManagerCompat; import java.util.ArrayList; import java.util.List; @@ -48,7 +49,8 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]); final int state; if (LauncherModel.isValidProvider(provider)) { - state = LauncherAppWidgetInfo.RESTORE_COMPLETED; + // This will ensure that we show 'Click to setup' UI if required. + state = LauncherAppWidgetInfo.FLAG_UI_NOT_READY; } else { state = LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; } diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 99a98ddac..440e4e7b9 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -355,7 +355,7 @@ public class AutoInstallsLayout { return addShortcut(info.loadLabel(mPackageManager).toString(), intent, Favorites.ITEM_TYPE_APPLICATION); } catch (PackageManager.NameNotFoundException e) { - if (LOGD) Log.w(TAG, "Unable to add favorite: " + packageName + "/" + className, e); + Log.e(TAG, "Unable to add favorite: " + packageName + "/" + className, e); } return -1; } else { @@ -367,7 +367,7 @@ public class AutoInstallsLayout { * Helper method to allow extending the parser capabilities */ protected long invalidPackageOrClass(XmlResourceParser parser) { - if (LOGD) Log.d(TAG, "Skipping invalid <favorite> with no component"); + Log.w(TAG, "Skipping invalid <favorite> with no component"); return -1; } } diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java index c8de9df10..c11824054 100644 --- a/src/com/android/launcher3/BaseContainerView.java +++ b/src/com/android/launcher3/BaseContainerView.java @@ -34,10 +34,12 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab // The bounds of the search bar. Only the left, top, right are used to inset the // search bar and the height is determined by the measurement of the layout private Rect mFixedSearchBarBounds = new Rect(); - // The bounds of the container + // The computed bounds of the search bar + private Rect mSearchBarBounds = new Rect(); + // The computed bounds of the container protected Rect mContentBounds = new Rect(); - // The padding to apply to the container to achieve the bounds - protected Rect mContentPadding = new Rect(); + // The computed padding to apply to the container to achieve the container bounds + private Rect mContentPadding = new Rect(); // The inset to apply to the edges and between the search bar and the container private int mContainerBoundsInset; private boolean mHasSearchBar; @@ -90,7 +92,7 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab */ protected void updateBackgroundAndPaddings() { Rect padding; - Rect searchBarBounds = new Rect(mFixedSearchBarBounds); + Rect searchBarBounds = new Rect(); if (!isValidSearchBarBounds(mFixedSearchBarBounds)) { // Use the default bounds padding = new Rect(mInsets.left + mContainerBoundsInset, @@ -110,14 +112,20 @@ public abstract class BaseContainerView extends LinearLayout implements Insettab (mHasSearchBar ? 0 : (mInsets.top + mContainerBoundsInset)), getMeasuredWidth() - mFixedSearchBarBounds.right, mInsets.bottom + mContainerBoundsInset); + + // Use the search bounds + searchBarBounds.set(mFixedSearchBarBounds); } - if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mFixedSearchBarBounds)) { + + // If either the computed container padding has changed, or the computed search bar bounds + // has changed, then notify the container + if (!padding.equals(mContentPadding) || !searchBarBounds.equals(mSearchBarBounds)) { mContentPadding.set(padding); mContentBounds.set(padding.left, padding.top, getMeasuredWidth() - padding.right, getMeasuredHeight() - padding.bottom); - mFixedSearchBarBounds.set(searchBarBounds); - onUpdateBackgroundAndPaddings(mFixedSearchBarBounds, padding); + mSearchBarBounds.set(searchBarBounds); + onUpdateBackgroundAndPaddings(mSearchBarBounds, padding); } } diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index 0fae427e8..f0d8b3b3d 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -92,9 +92,15 @@ public abstract class BaseRecyclerView extends RecyclerView // TODO(winsonc): If we want to animate the section heads while scrolling, we can // initiate that here if the recycler view scroll state is not // RecyclerView.SCROLL_STATE_IDLE. + + onUpdateScrollbar(dy); } } + public void reset() { + mScrollbar.reattachThumbToScroll(); + } + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -143,7 +149,7 @@ public abstract class BaseRecyclerView extends RecyclerView mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); break; } - return mScrollbar.isDragging(); + return mScrollbar.isDraggingThumb(); } public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { @@ -185,12 +191,10 @@ public abstract class BaseRecyclerView extends RecyclerView * AvailableScrollHeight = Total height of the all items - last page height * * This assumes that all rows are the same height. - * - * @param yOffset the offset from the top of the recycler view to start tracking. */ - protected int getAvailableScrollHeight(int rowCount, int rowHeight, int yOffset) { + protected int getAvailableScrollHeight(int rowCount, int rowHeight) { int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom; - int scrollHeight = getPaddingTop() + yOffset + rowCount * rowHeight + getPaddingBottom(); + int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom(); int availableScrollHeight = scrollHeight - visibleHeight; return availableScrollHeight; } @@ -222,7 +226,7 @@ public abstract class BaseRecyclerView extends RecyclerView @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - onUpdateScrollbar(); + onUpdateScrollbar(0); mScrollbar.draw(canvas); } @@ -234,24 +238,21 @@ public abstract class BaseRecyclerView extends RecyclerView * @param scrollPosState the current scroll position * @param rowCount the number of rows, used to calculate the total scroll height (assumes that * all rows are the same height) - * @param yOffset the offset to start tracking in the recycler view (only used for all apps) */ protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState, - int rowCount, int yOffset) { - int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight, - yOffset); - int availableScrollBarHeight = getAvailableScrollBarHeight(); - + int rowCount) { // Only show the scrollbar if there is height to be scrolled + int availableScrollBarHeight = getAvailableScrollBarHeight(); + int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight); if (availableScrollHeight <= 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } // Calculate the current scroll position, the scrollY of the recycler view accounts for the // view padding, while the scrollBarY is drawn right up to the background padding (ignoring // padding) - int scrollY = getPaddingTop() + yOffset + + int scrollY = getPaddingTop() + (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset; int scrollBarY = mBackgroundPadding.top + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); @@ -261,9 +262,9 @@ public abstract class BaseRecyclerView extends RecyclerView if (Utilities.isRtl(getResources())) { scrollBarX = mBackgroundPadding.left; } else { - scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getWidth(); + scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); } - mScrollbar.setScrollbarThumbOffset(scrollBarX, scrollBarY); + mScrollbar.setThumbOffset(scrollBarX, scrollBarY); } /** @@ -276,10 +277,15 @@ public abstract class BaseRecyclerView extends RecyclerView * Updates the bounds for the scrollbar. * <p>Override in each subclass of this base class. */ - public abstract void onUpdateScrollbar(); + public abstract void onUpdateScrollbar(int dy); /** * <p>Override in each subclass of this base class. */ public void onFastScrollCompleted() {} + + /** + * Returns information about the item that the recycler view is currently scrolled to. + */ + protected abstract void getCurScrollState(ScrollPositionState stateOut); }
\ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index 2c4184dc4..fcee7e8dd 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; @@ -51,14 +52,21 @@ public class BaseRecyclerViewFastScrollBar { private int mThumbActiveColor; @Thunk Point mThumbOffset = new Point(-1, -1); @Thunk Paint mThumbPaint; - private Paint mTrackPaint; private int mThumbMinWidth; private int mThumbMaxWidth; @Thunk int mThumbWidth; @Thunk int mThumbHeight; + private int mThumbCurvature; + private Path mThumbPath = new Path(); + private Paint mTrackPaint; + private int mTrackWidth; + private float mLastTouchY; // The inset is the buffer around which a point will still register as a click on the scrollbar private int mTouchInset; private boolean mIsDragging; + private boolean mIsThumbDetached; + private boolean mCanThumbDetach; + private boolean mIgnoreDragGesture; // This is the offset from the top of the scrollbar when the user first starts touching. To // prevent jumping, this offset is applied as the user scrolls. @@ -72,51 +80,74 @@ public class BaseRecyclerViewFastScrollBar { mPopup = new BaseRecyclerViewFastScrollPopup(rv, res); mTrackPaint = new Paint(); mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK)); - mTrackPaint.setAlpha(0); + mTrackPaint.setAlpha(MAX_TRACK_ALPHA); mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor( res.getColor(R.color.container_fastscroll_thumb_inactive_color)); mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color); mThumbPaint = new Paint(); + mThumbPaint.setAntiAlias(true); mThumbPaint.setColor(mThumbInactiveColor); + mThumbPaint.setStyle(Paint.Style.FILL); mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width); mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width); mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height); + mThumbCurvature = mThumbMaxWidth - mThumbMinWidth; mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset); } - public void setScrollbarThumbOffset(int x, int y) { + public void setDetachThumbOnFastScroll() { + mCanThumbDetach = true; + } + + public void reattachThumbToScroll() { + mIsThumbDetached = false; + } + + public void setThumbOffset(int x, int y) { if (mThumbOffset.x == x && mThumbOffset.y == y) { return; } - mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y, + mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); mThumbOffset.set(x, y); - mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, - mRv.getHeight())); + updateThumbPath(); + mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y, + mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); mRv.invalidate(mInvalidateRect); } - // Setter/getter for the search bar width for animations - public void setWidth(int width) { - mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + public Point getThumbOffset() { + return mThumbOffset; + } + + // Setter/getter for the thumb bar width for animations + public void setThumbWidth(int width) { + mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y, + mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); mThumbWidth = width; - mInvalidateRect.union(new Rect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, - mRv.getHeight())); + updateThumbPath(); + mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y, + mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); mRv.invalidate(mInvalidateRect); } - public int getWidth() { + public int getThumbWidth() { return mThumbWidth; } - // Setter/getter for the track background alpha for animations - public void setTrackAlpha(int alpha) { - mTrackPaint.setAlpha(alpha); - mInvalidateRect.set(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight()); + // Setter/getter for the track bar width for animations + public void setTrackWidth(int width) { + mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight()); + mTrackWidth = width; + updateThumbPath(); + mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth, + mRv.getHeight()); mRv.invalidate(mInvalidateRect); } - public int getTrackAlpha() { - return mTrackPaint.getAlpha(); + public int getTrackWidth() { + return mTrackWidth; } public int getThumbHeight() { @@ -127,10 +158,18 @@ public class BaseRecyclerViewFastScrollBar { return mThumbMaxWidth; } - public boolean isDragging() { + public float getLastTouchY() { + return mLastTouchY; + } + + public boolean isDraggingThumb() { return mIsDragging; } + public boolean isThumbDetached() { + return mIsThumbDetached; + } + /** * Handles the touch event and determines whether to show the fast scroller (or updates it if * it is already showing). @@ -142,16 +181,21 @@ public class BaseRecyclerViewFastScrollBar { int y = (int) ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: - if (isNearPoint(downX, downY)) { + if (isNearThumb(downX, downY)) { mTouchOffset = downY - mThumbOffset.y; } break; case MotionEvent.ACTION_MOVE: - // Check if we should start scrolling - if (!mIsDragging && isNearPoint(downX, downY) && + // Check if we should start scrolling, but ignore this fastscroll gesture if we have + // exceeded some fixed movement + mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop(); + if (!mIsDragging && !mIgnoreDragGesture && isNearThumb(downX, lastY) && Math.abs(y - downY) > config.getScaledTouchSlop()) { mRv.getParent().requestDisallowInterceptTouchEvent(true); mIsDragging = true; + if (mCanThumbDetach) { + mIsThumbDetached = true; + } mTouchOffset += (lastY - downY); mPopup.animateVisibility(true); animateScrollbar(true); @@ -166,11 +210,14 @@ public class BaseRecyclerViewFastScrollBar { mPopup.setSectionName(sectionName); mPopup.animateVisibility(!sectionName.isEmpty()); mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY)); + mLastTouchY = boundedY; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mTouchOffset = 0; + mLastTouchY = 0; + mIgnoreDragGesture = false; if (mIsDragging) { mIsDragging = false; mPopup.animateVisibility(false); @@ -189,8 +236,7 @@ public class BaseRecyclerViewFastScrollBar { if (mTrackPaint.getAlpha() > 0) { canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint); } - canvas.drawRect(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, - mThumbOffset.y + mThumbHeight, mThumbPaint); + canvas.drawPath(mThumbPath, mThumbPaint); // Draw the popup mPopup.draw(canvas); @@ -203,30 +249,49 @@ public class BaseRecyclerViewFastScrollBar { if (mScrollbarAnimator != null) { mScrollbarAnimator.cancel(); } - ObjectAnimator trackAlphaAnim = ObjectAnimator.ofInt(this, "trackAlpha", - isScrolling ? MAX_TRACK_ALPHA : 0); - ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "width", - isScrolling ? mThumbMaxWidth : mThumbMinWidth); - ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), - mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor); - colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - mThumbPaint.setColor((Integer) animator.getAnimatedValue()); - mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, - mThumbOffset.y + mThumbHeight); - } - }); + mScrollbarAnimator = new AnimatorSet(); - mScrollbarAnimator.playTogether(trackAlphaAnim, thumbWidthAnim, colorAnimation); + ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth", + isScrolling ? mThumbMaxWidth : mThumbMinWidth); + ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth", + isScrolling ? mThumbMaxWidth : mThumbMinWidth); + mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim); + if (mThumbActiveColor != mThumbInactiveColor) { + ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), + mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor); + colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + mThumbPaint.setColor((Integer) animator.getAnimatedValue()); + mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, + mThumbOffset.y + mThumbHeight); + } + }); + mScrollbarAnimator.play(colorAnimation); + } mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION); mScrollbarAnimator.start(); } /** + * Updates the path for the thumb drawable. + */ + private void updateThumbPath() { + mThumbCurvature = mThumbMaxWidth - mThumbWidth; + mThumbPath.reset(); + mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y); // tr + mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); // br + mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight); // bl + mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight, + mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2, + mThumbOffset.x, mThumbOffset.y); // bl2tl + mThumbPath.close(); + } + + /** * Returns whether the specified points are near the scroll bar bounds. */ - private boolean isNearPoint(int x, int y) { + private boolean isNearThumb(int x, int y) { mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); mTmpRect.inset(mTouchInset, mTouchInset); diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index a0be8ea2b..507087824 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -34,11 +34,13 @@ import android.util.SparseArray; import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.TextView; + import com.android.launcher3.IconCache.IconLoadRequest; import com.android.launcher3.model.PackageItemInfo; @@ -508,7 +510,7 @@ public class BubbleTextView extends TextView mIcon.setBounds(0, 0, iconSize, iconSize); } if (mLayoutHorizontal) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { setCompoundDrawablesRelative(mIcon, null, null, null); } else { setCompoundDrawables(mIcon, null, null, null); @@ -538,6 +540,13 @@ public class BubbleTextView extends TextView } else if (info instanceof ShortcutInfo) { applyFromShortcutInfo((ShortcutInfo) info, LauncherAppState.getInstance().getIconCache()); + if ((info.rank < FolderIcon.NUM_ITEMS_IN_PREVIEW) && (info.container >= 0)) { + View folderIcon = + mLauncher.getWorkspace().getHomescreenIconByItemId(info.container); + if (folderIcon != null) { + folderIcon.invalidate(); + } + } } else if (info instanceof PackageItemInfo) { applyFromPackageItemInfo((PackageItemInfo) info); } diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index b7f89d02a..2baa6d8ee 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -24,7 +24,6 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; -import android.content.res.Configuration; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.PointF; @@ -93,7 +92,7 @@ public abstract class ButtonDropTarget extends TextView // drawableLeft and drawableStart. mDrawable = getResources().getDrawable(resId); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); } else { setCompoundDrawablesWithIntrinsicBounds(mDrawable, null, null, null); @@ -114,7 +113,7 @@ public abstract class ButtonDropTarget extends TextView @Override public final void onDragEnter(DragObject d) { d.dragView.setColor(mHoverColor); - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { animateTextColor(mHoverColor); } else { if (mCurrentFilter == null) { @@ -131,8 +130,8 @@ public abstract class ButtonDropTarget extends TextView // Do nothing } - protected void resetHoverColor() { - if (Utilities.isLmpOrAbove()) { + protected void resetHoverColor() { + if (Utilities.ATLEAST_LOLLIPOP) { animateTextColor(mOriginalTextColor.getDefaultColor()); } else { mDrawable.setColorFilter(null); diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 809688712..84e2d49c2 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -54,7 +54,6 @@ import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; import com.android.launcher3.accessibility.FolderAccessibilityHelper; import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; import com.android.launcher3.util.Thunk; -import com.android.launcher3.widget.PendingAddWidgetInfo; import java.util.ArrayList; import java.util.Arrays; @@ -211,6 +210,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mBackground = (TransitionDrawable) res.getDrawable(R.drawable.bg_screenpanel); mBackground.setCallback(this); + mBackground.setAlpha((int) (mBackgroundAlpha * 255)); mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * grid.iconSizePx); @@ -414,7 +414,11 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { if (mIsDragOverlapping) { mBackground.startTransition(BACKGROUND_ACTIVATE_DURATION); } else { - mBackground.reverseTransition(BACKGROUND_ACTIVATE_DURATION); + if (mBackgroundAlpha > 0f) { + mBackground.reverseTransition(BACKGROUND_ACTIVATE_DURATION); + } else { + mBackground.resetTransition(); + } } invalidate(); } @@ -2681,65 +2685,6 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { resultRect.set(x, y, x + width, y + height); } - /** - * Computes the required horizontal and vertical cell spans to always - * fit the given rectangle. - * - * @param width Width in pixels - * @param height Height in pixels - * @param result An array of length 2 in which to store the result (may be null). - */ - public static int[] rectToCell(Launcher launcher, int width, int height, int[] result) { - return rectToCell(launcher.getDeviceProfile(), launcher, width, height, result); - } - - public static int[] rectToCell(DeviceProfile grid, Context context, int width, int height, - int[] result) { - Rect padding = grid.getWorkspacePadding(Utilities.isRtl(context.getResources())); - - // Always assume we're working with the smallest span to make sure we - // reserve enough space in both orientations. - int parentWidth = DeviceProfile.calculateCellWidth(grid.widthPx - - padding.left - padding.right, (int) grid.inv.numColumns); - int parentHeight = DeviceProfile.calculateCellHeight(grid.heightPx - - padding.top - padding.bottom, (int) grid.inv.numRows); - int smallerSize = Math.min(parentWidth, parentHeight); - - // Always round up to next largest cell - int spanX = (int) Math.ceil(width / (float) smallerSize); - int spanY = (int) Math.ceil(height / (float) smallerSize); - - if (result == null) { - return new int[] { spanX, spanY }; - } - result[0] = spanX; - result[1] = spanY; - return result; - } - - /** - * Calculate the grid spans needed to fit given item - */ - public void calculateSpans(ItemInfo info) { - final int minWidth; - final int minHeight; - - if (info instanceof LauncherAppWidgetInfo) { - minWidth = ((LauncherAppWidgetInfo) info).minWidth; - minHeight = ((LauncherAppWidgetInfo) info).minHeight; - } else if (info instanceof PendingAddWidgetInfo) { - minWidth = ((PendingAddWidgetInfo) info).minWidth; - minHeight = ((PendingAddWidgetInfo) info).minHeight; - } else { - // It's not a widget, so it must be 1x1 - info.spanX = info.spanY = 1; - return; - } - int[] spans = rectToCell(mLauncher, minWidth, minHeight, null); - info.spanX = spans[0]; - info.spanY = spans[1]; - } - private void clearOccupiedCells() { for (int x = 0; x < mCountX; x++) { for (int y = 0; y < mCountY; y++) { diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 62b05b0d6..774594fe2 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -56,7 +56,6 @@ public class DeviceProfile { private final int overviewModeBarItemWidthPx; private final int overviewModeBarSpacerWidthPx; private final float overviewModeIconZoneRatio; - private final float overviewModeScaleFactor; // Workspace private int desiredWorkspaceLeftRightMarginPx; @@ -136,8 +135,6 @@ public class DeviceProfile { res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width); overviewModeIconZoneRatio = res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f; - overviewModeScaleFactor = - res.getInteger(R.integer.config_dynamic_grid_overview_scale_percentage) / 100f; iconDrawablePaddingOriginalPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding); @@ -334,18 +331,11 @@ public class DeviceProfile { } } - Rect getOverviewModeButtonBarRect() { + int getOverviewModeButtonBarHeight() { int zoneHeight = (int) (overviewModeIconZoneRatio * availableHeightPx); zoneHeight = Math.min(overviewModeMaxIconZoneHeightPx, Math.max(overviewModeMinIconZoneHeightPx, zoneHeight)); - return new Rect(0, availableHeightPx - zoneHeight, 0, availableHeightPx); - } - - public float getOverviewModeScale(boolean isLayoutRtl) { - Rect workspacePadding = getWorkspacePadding(isLayoutRtl); - Rect overviewBar = getOverviewModeButtonBarRect(); - int pageSpace = availableHeightPx - workspacePadding.top - workspacePadding.bottom; - return (overviewModeScaleFactor * (pageSpace - overviewBar.height())) / pageSpace; + return zoneHeight; } // The rect returned will be extended to below the system ui that covers the workspace @@ -394,7 +384,7 @@ public class DeviceProfile { final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources()); // Layout the search bar space - View searchBar = launcher.getSearchBar(); + View searchBar = launcher.getSearchDropTargetBar(); lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams(); if (hasVerticalBarLayout) { // Vertical search bar space -- The search bar is fixed in the layout to be on the left @@ -476,7 +466,7 @@ public class DeviceProfile { // Layout the Overview Mode ViewGroup overviewMode = launcher.getOverviewPanel(); if (overviewMode != null) { - Rect r = getOverviewModeButtonBarRect(); + int overviewButtonBarHeight = getOverviewModeButtonBarHeight(); lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams(); lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; @@ -485,7 +475,7 @@ public class DeviceProfile { int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx; lp.width = Math.min(availableWidthPx, maxWidth); - lp.height = r.height(); + lp.height = overviewButtonBarHeight; overviewMode.setLayoutParams(lp); if (lp.width > totalItemWidth && visibleChildCount > 1) { diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java index aaa14e6a6..1c18747c1 100644 --- a/src/com/android/launcher3/DragLayer.java +++ b/src/com/android/launcher3/DragLayer.java @@ -159,7 +159,7 @@ public class DragLayer extends InsettableFrameLayout { } private boolean isEventOverDropTargetBar(MotionEvent ev) { - getDescendantRectRelativeToSelf(mLauncher.getSearchBar(), mHitRect); + getDescendantRectRelativeToSelf(mLauncher.getSearchDropTargetBar(), mHitRect); if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) { return true; } @@ -321,7 +321,7 @@ public class DragLayer extends InsettableFrameLayout { childrenForAccessibility.add(currentFolder); if (isInAccessibleDrag()) { - childrenForAccessibility.add(mLauncher.getSearchBar()); + childrenForAccessibility.add(mLauncher.getSearchDropTargetBar()); } } else { super.addChildrenForAccessibility(childrenForAccessibility); diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java index dfa8202a7..2acfc6140 100644 --- a/src/com/android/launcher3/DragView.java +++ b/src/com/android/launcher3/DragView.java @@ -131,7 +131,7 @@ public class DragView extends View { measure(ms, ms); mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { setElevation(getResources().getDimension(R.dimen.drag_elevation)); } } @@ -252,14 +252,14 @@ public class DragView extends View { setColorScale(color, m2); m1.postConcat(m2); - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { animateFilterTo(m1.getArray()); } else { mPaint.setColorFilter(new ColorMatrixColorFilter(m1)); invalidate(); } } else { - if (!Utilities.isLmpOrAbove() || mCurrentFilter == null) { + if (!Utilities.ATLEAST_LOLLIPOP || mCurrentFilter == null) { mPaint.setColorFilter(null); invalidate(); } else { diff --git a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java b/src/com/android/launcher3/ExtendedEditText.java index b7dcd66ed..c7b64ec7d 100644 --- a/src/com/android/launcher3/allapps/AllAppsSearchEditView.java +++ b/src/com/android/launcher3/ExtendedEditText.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.allapps; +package com.android.launcher3; import android.content.Context; import android.util.AttributeSet; @@ -22,28 +22,28 @@ import android.widget.EditText; /** - * The edit text for the search container + * The edit text that reports back when the back key has been pressed. */ -public class AllAppsSearchEditView extends EditText { +public class ExtendedEditText extends EditText { /** * Implemented by listeners of the back key. */ public interface OnBackKeyListener { - public void onBackKey(); + public boolean onBackKey(); } private OnBackKeyListener mBackKeyListener; - public AllAppsSearchEditView(Context context) { - this(context, null); + public ExtendedEditText(Context context) { + super(context); } - public AllAppsSearchEditView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + public ExtendedEditText(Context context, AttributeSet attrs) { + super(context, attrs); } - public AllAppsSearchEditView(Context context, AttributeSet attrs, int defStyleAttr) { + public ExtendedEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @@ -56,7 +56,7 @@ public class AllAppsSearchEditView extends EditText { // If this is a back key, propagate the key back to the listener if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { if (mBackKeyListener != null) { - mBackKeyListener.onBackKey(); + return mBackKeyListener.onBackKey(); } return false; } diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java index 2e19f6eba..c1aa35669 100644 --- a/src/com/android/launcher3/Folder.java +++ b/src/com/android/launcher3/Folder.java @@ -124,7 +124,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Thunk FolderPagedView mContent; @Thunk View mContentWrapper; - FolderEditText mFolderName; + ExtendedEditText mFolderName; private View mFooter; private int mFooterHeight; @@ -196,8 +196,15 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList mContent = (FolderPagedView) findViewById(R.id.folder_content); mContent.setFolder(this); - mFolderName = (FolderEditText) findViewById(R.id.folder_name); - mFolderName.setFolder(this); + mFolderName = (ExtendedEditText) findViewById(R.id.folder_name); + mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() { + @Override + public boolean onBackKey() { + // Close the activity on back key press + doneEditingFolderName(true); + return false; + } + }); mFolderName.setOnFocusChangeListener(this); // We disable action mode for now since it messes up the view on phones @@ -275,7 +282,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList @Override public void enableAccessibleDrag(boolean enable) { - mLauncher.getSearchBar().enableAccessibleDrag(enable); + mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable); for (int i = 0; i < mContent.getChildCount(); i++) { mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG); } @@ -446,7 +453,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList Animator openFolderAnim = null; final Runnable onCompleteRunnable; - if (!Utilities.isLmpOrAbove()) { + if (!Utilities.ATLEAST_LOLLIPOP) { positionAndSizeAsIcon(); centerAboutIcon(); @@ -561,7 +568,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList public void onAnimationEnd(Animator animation) { mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION) .translationX(0) - .setInterpolator(Utilities.isLmpOrAbove() ? + .setInterpolator(Utilities.ATLEAST_LOLLIPOP ? AnimationUtils.loadInterpolator(mLauncher, android.R.interpolator.fast_out_slow_in) : new LogDecelerateInterpolator(100, 0)); @@ -1389,7 +1396,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList } // Compares item position based on rank and position giving priority to the rank. - private static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() { + public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() { @Override public int compare(ItemInfo lhs, ItemInfo rhs) { diff --git a/src/com/android/launcher3/FolderEditText.java b/src/com/android/launcher3/FolderEditText.java deleted file mode 100644 index c31100899..000000000 --- a/src/com/android/launcher3/FolderEditText.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.android.launcher3; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.widget.EditText; - -public class FolderEditText extends EditText { - - private Folder mFolder; - - public FolderEditText(Context context) { - super(context); - } - - public FolderEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FolderEditText(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public void setFolder(Folder folder) { - mFolder = folder; - } - - @Override - public boolean onKeyPreIme(int keyCode, KeyEvent event) { - // Catch the back button on the soft keyboard so that we can just close the activity - if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK) { - mFolder.doneEditingFolderName(true); - } - return super.onKeyPreIme(keyCode, event); - } -} diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java index f2ec1b68c..cc9c5738a 100644 --- a/src/com/android/launcher3/FolderPagedView.java +++ b/src/com/android/launcher3/FolderPagedView.java @@ -329,6 +329,10 @@ public class FolderPagedView extends PagedView { lp.cellY = info.cellY; currentPage.addViewToCellLayout( v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); + + if (rank < FolderIcon.NUM_ITEMS_IN_PREVIEW && v instanceof BubbleTextView) { + ((BubbleTextView) v).verifyHighRes(); + } } rank ++; diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java index 916418f18..59ab8397d 100644 --- a/src/com/android/launcher3/IconCache.java +++ b/src/com/android/launcher3/IconCache.java @@ -540,7 +540,7 @@ public class IconCache { mCache.put(cacheKey, entry); // Check the DB first. - if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) { + if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { if (info != null) { entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext); } else { @@ -579,7 +579,14 @@ public class IconCache { Bitmap icon, CharSequence title) { removeFromMemCacheLocked(packageName, user); - CacheEntry entry = getEntryForPackageLocked(packageName, user, false); + ComponentKey cacheKey = getPackageKey(packageName, user); + CacheEntry entry = mCache.get(cacheKey); + + // For icon caching, do not go through DB. Just update the in-memory entry. + if (entry == null) { + entry = new CacheEntry(); + mCache.put(cacheKey, entry); + } if (!TextUtils.isEmpty(title)) { entry.title = title; } @@ -588,15 +595,18 @@ public class IconCache { } } + private static ComponentKey getPackageKey(String packageName, UserHandleCompat user) { + ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); + return new ComponentKey(cn, user); + } + /** * Gets an entry for the package, which can be used as a fallback entry for various components. * This method is not thread safe, it must be called from a synchronized method. - * */ private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user, boolean useLowResIcon) { - ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME); - ComponentKey cacheKey = new ComponentKey(cn, user); + ComponentKey cacheKey = getPackageKey(packageName, user); CacheEntry entry = mCache.get(cacheKey); if (entry == null || (entry.isLowResIcon && !useLowResIcon)) { @@ -604,9 +614,11 @@ public class IconCache { boolean entryUpdated = true; // Check the DB first. - if (!getEntryFromDB(cn, user, entry, useLowResIcon)) { + if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) { try { - PackageInfo info = mPackageManager.getPackageInfo(packageName, 0); + int flags = UserHandleCompat.myUserHandle().equals(user) ? 0 : + PackageManager.GET_UNINSTALLED_PACKAGES; + PackageInfo info = mPackageManager.getPackageInfo(packageName, flags); ApplicationInfo appInfo = info.applicationInfo; if (appInfo == null) { throw new NameNotFoundException("ApplicationInfo is null"); @@ -622,7 +634,8 @@ public class IconCache { // package updates. ContentValues values = newContentValues(entry.icon, entry.title.toString(), mPackageBgColor); - addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user)); + addIconToDB(values, cacheKey.componentName, info, + mUserManager.getSerialNumberForUser(user)); } catch (NameNotFoundException e) { if (DEBUG) Log.d(TAG, "Application not installed " + packageName); @@ -649,7 +662,7 @@ public class IconCache { * @param dpi the native density of the icon */ public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label, - long userSerial) { + long userSerial, InvariantDeviceProfile idp) { // TODO rescale to the correct native DPI try { PackageManager packageManager = mContext.getPackageManager(); @@ -660,21 +673,22 @@ public class IconCache { // pass } - ContentValues values = newContentValues(icon, label, Color.TRANSPARENT); + ContentValues values = newContentValues( + Bitmap.createScaledBitmap(icon, idp.iconBitmapSize, idp.iconBitmapSize, true), + label, Color.TRANSPARENT); values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString()); values.put(IconDB.COLUMN_USER, userSerial); mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); } - private boolean getEntryFromDB(ComponentName component, UserHandleCompat user, - CacheEntry entry, boolean lowRes) { + private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) { Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME, new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON, IconDB.COLUMN_LABEL}, IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?", - new String[] {component.flattenToString(), - Long.toString(mUserManager.getSerialNumberForUser(user))}, + new String[] {cacheKey.componentName.flattenToString(), + Long.toString(mUserManager.getSerialNumberForUser(cacheKey.user))}, null, null, null); try { if (c.moveToNext()) { @@ -685,7 +699,8 @@ public class IconCache { entry.title = ""; entry.contentDescription = ""; } else { - entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user); + entry.contentDescription = mUserManager.getBadgedLabelForUser( + entry.title, cacheKey.user); } return true; } @@ -774,7 +789,7 @@ public class IconCache { } private static final class IconDB extends SQLiteOpenHelper { - private final static int DB_VERSION = 6; + private final static int DB_VERSION = 7; private final static String TABLE_NAME = "icons"; private final static String COLUMN_ROWID = "rowid"; diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 6648b6e74..1f843cb70 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -116,8 +116,6 @@ import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collection; @@ -132,8 +130,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, - View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener, - LauncherStateTransitionAnimation.Callbacks { + View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { static final String TAG = "Launcher"; static final boolean LOGD = false; @@ -304,6 +301,8 @@ public class Launcher extends Activity private boolean mHasFocus = false; private boolean mAttached = false; + private LauncherClings mClings; + private static LongArrayMap<FolderInfo> sFolders = new LongArrayMap<>(); private View.OnTouchListener mHapticFeedbackTouchListener; @@ -361,18 +360,6 @@ public class Launcher extends Activity } } - // TODO: remove this field and call method directly when Launcher3 can depend on M APIs - private static Method sClipRevealMethod = null; - static { - Class<?> activityOptionsClass = ActivityOptions.class; - try { - sClipRevealMethod = activityOptionsClass.getDeclaredMethod("makeClipRevealAnimation", - View.class, int.class, int.class, int.class, int.class); - } catch (Exception e) { - // Earlier version - } - } - @Thunk Runnable mBuildLayersRunnable = new Runnable() { public void run() { if (mWorkspace != null) { @@ -452,7 +439,7 @@ public class Launcher extends Activity mDragController = new DragController(this); mInflater = getLayoutInflater(); - mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this); + mStateTransitionAnimation = new LauncherStateTransitionAnimation(this); mStats = new Stats(this); @@ -663,12 +650,12 @@ public class Launcher extends Activity public boolean isDraggingEnabled() { // We prevent dragging when we are loading the workspace as it is possible to pick up a view // that is subsequently removed from the workspace in startBinding(). - return !mModel.isLoadingWorkspace(); + return !isWorkspaceLoading(); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static int generateViewId() { - if (Build.VERSION.SDK_INT >= 17) { + if (Utilities.ATLEAST_JB_MR1) { return View.generateViewId(); } else { // View.generateViewId() is not available. The following fallback logic is a copy @@ -743,6 +730,7 @@ public class Launcher extends Activity }; if (requestCode == REQUEST_BIND_APPWIDGET) { + // This is called only if the user did not previously have permissions to bind widgets final int appWidgetId = data != null ? data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) : -1; if (resultCode == RESULT_CANCELED) { @@ -752,6 +740,10 @@ public class Launcher extends Activity } else if (resultCode == RESULT_OK) { addAppWidgetImpl(appWidgetId, mPendingAddInfo, null, mPendingAddWidgetInfo, ON_ACTIVITY_RESULT_ANIMATION_DELAY); + + // When the user has granted permission to bind widgets, we should check to see if + // we can inflate the default search bar widget. + getOrCreateQsbBar(); } return; } else if (requestCode == REQUEST_PICK_WALLPAPER) { @@ -1004,6 +996,12 @@ public class Launcher extends Activity mPaused = false; if (mRestoring || mOnResumeNeedsLoad) { setWorkspaceLoading(true); + + // If we're starting binding all over again, clear any bind calls we'd postponed in + // the past (see waitUntilResume) -- we don't need them since we're starting binding + // from scratch again + mBindOnResumeCallbacks.clear(); + mModel.startLoader(PagedView.INVALID_RESTORE_PAGE); mRestoring = false; mOnResumeNeedsLoad = false; @@ -1553,23 +1551,6 @@ public class Launcher extends Activity } } - private int[] getSpanForWidget(ComponentName component, int minWidth, int minHeight) { - Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(this, component, null); - // We want to account for the extra amount of padding that we are adding to the widget - // to ensure that it gets the full amount of space that it has requested - int requiredWidth = minWidth + padding.left + padding.right; - int requiredHeight = minHeight + padding.top + padding.bottom; - return CellLayout.rectToCell(this, requiredWidth, requiredHeight, null); - } - - public int[] getSpanForWidget(AppWidgetProviderInfo info) { - return getSpanForWidget(info.provider, info.minWidth, info.minHeight); - } - - public int[] getMinSpanForWidget(AppWidgetProviderInfo info) { - return getSpanForWidget(info.provider, info.minResizeWidth, info.minResizeHeight); - } - /** * Add a widget to the workspace. * @@ -1666,18 +1647,18 @@ public class Launcher extends Activity } registerReceiver(mReceiver, filter); FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView()); - setupTransparentSystemBarsForLmp(); + setupTransparentSystemBarsForLollipop(); mAttached = true; mVisible = true; } /** - * Sets up transparent navigation and status bars in LMP. + * Sets up transparent navigation and status bars in Lollipop. * This method is a no-op for other platform versions. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void setupTransparentSystemBarsForLmp() { - if (Utilities.isLmpOrAbove()) { + private void setupTransparentSystemBarsForLollipop() { + if (Utilities.ATLEAST_LOLLIPOP) { Window window = getWindow(); window.getAttributes().systemUiVisibility |= (View.SYSTEM_UI_FLAG_LAYOUT_STABLE @@ -1844,7 +1825,7 @@ public class Launcher extends Activity return mOverviewPanel; } - public SearchDropTargetBar getSearchBar() { + public SearchDropTargetBar getSearchDropTargetBar() { return mSearchDropTargetBar; } @@ -1880,29 +1861,22 @@ public class Launcher extends Activity super.onNewIntent(intent); // Close the menu - if (Intent.ACTION_MAIN.equals(intent.getAction())) { + Folder openFolder = mWorkspace.getOpenFolder(); + boolean alreadyOnHome = mHasFocus && ((intent.getFlags() & + Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) + != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); + boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction()); + if (isActionMain) { // also will cancel mWaitingForResult. closeSystemDialogs(); - final boolean alreadyOnHome = mHasFocus && ((intent.getFlags() & - Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) - != Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - if (mWorkspace == null) { // Can be cases where mWorkspace is null, this prevents a NPE return; } - Folder openFolder = mWorkspace.getOpenFolder(); // In all these cases, only animate if we're already on home mWorkspace.exitWidgetResizeMode(); - boolean moveToDefaultScreen = mLauncherCallbacks != null ? - mLauncherCallbacks.shouldMoveToDefaultScreenOnHomeIntent() : true; - if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() && - openFolder == null && moveToDefaultScreen) { - mWorkspace.moveToDefaultScreen(true); - } - closeFolder(); exitSpringLoadedDragMode(); @@ -1936,13 +1910,30 @@ public class Launcher extends Activity } } - if (DEBUG_RESUME_TIME) { - Log.d(TAG, "Time spent in onNewIntent: " + (System.currentTimeMillis() - startTime)); - } - if (mLauncherCallbacks != null) { mLauncherCallbacks.onNewIntent(intent); } + + // Defer moving to the default screen until after we callback to the LauncherCallbacks + // as slow logic in the callbacks eat into the time the scroller expects for the snapToPage + // animation. + if (isActionMain) { + boolean moveToDefaultScreen = mLauncherCallbacks != null ? + mLauncherCallbacks.shouldMoveToDefaultScreenOnHomeIntent() : true; + if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() && + openFolder == null && moveToDefaultScreen) { + mWorkspace.post(new Runnable() { + @Override + public void run() { + mWorkspace.moveToDefaultScreen(true); + } + }); + } + } + + if (DEBUG_RESUME_TIME) { + Log.d(TAG, "Time spent in onNewIntent: " + (System.currentTimeMillis() - startTime)); + } } @Override @@ -2150,6 +2141,15 @@ public class Launcher extends Activity } } + public void startSearchFromAllApps(View v, Intent searchIntent, String searchQuery) { + if (mLauncherCallbacks != null && mLauncherCallbacks.startSearchFromAllApps(searchQuery)) { + return; + } + + // If not handled, then just start the provided search intent + startActivitySafely(v, searchIntent, null); + } + public boolean isOnCustomContent() { return mWorkspace.isOnOrMovingToCustomContent(); } @@ -2218,7 +2218,7 @@ public class Launcher extends Activity mPendingAddInfo.screenId = -1; mPendingAddInfo.cellX = mPendingAddInfo.cellY = -1; mPendingAddInfo.spanX = mPendingAddInfo.spanY = -1; - mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = -1; + mPendingAddInfo.minSpanX = mPendingAddInfo.minSpanY = 1; mPendingAddInfo.dropPos = null; } @@ -2551,6 +2551,10 @@ public class Launcher extends Activity if (!isAppsViewVisible()) { showAppsView(true /* animated */, false /* resetListToTop */, true /* updatePredictedApps */, false /* focusSearchBar */); + + if (mLauncherCallbacks != null) { + mLauncherCallbacks.onClickAllAppsButton(v); + } } } @@ -2679,6 +2683,7 @@ public class Launcher extends Activity throw new IllegalArgumentException("Input must be a FolderIcon"); } + // TODO(sunnygoyal): Re-evaluate this code. FolderIcon folderIcon = (FolderIcon) v; final FolderInfo info = folderIcon.getFolderInfo(); Folder openFolder = mWorkspace.getFolderForTag(info); @@ -2882,8 +2887,7 @@ public class Launcher extends Activity Bundle optsBundle = null; if (useLaunchAnimation) { ActivityOptions opts = null; - if (sClipRevealMethod != null) { - // TODO: call method directly when Launcher3 can depend on M APIs + if (Utilities.ATLEAST_MARSHMALLOW) { int left = 0, top = 0; int width = v.getMeasuredWidth(), height = v.getMeasuredHeight(); if (v instanceof TextView) { @@ -2897,22 +2901,12 @@ public class Launcher extends Activity height = bounds.height(); } } - try { - opts = (ActivityOptions) sClipRevealMethod.invoke(null, v, - left, top, width, height); - } catch (IllegalAccessException e) { - Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); - sClipRevealMethod = null; - } catch (InvocationTargetException e) { - Log.d(TAG, "Could not call makeClipRevealAnimation: " + e); - sClipRevealMethod = null; - } - } - if (opts == null && !Utilities.isLmpOrAbove()) { + opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height); + } else if (!Utilities.ATLEAST_LOLLIPOP) { // Below L, we use a scale up animation opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight()); - } else if (opts == null && Utilities.isLmpMR1()) { + } else if (Utilities.ATLEAST_LOLLIPOP_MR1) { // On L devices, we use the device default slide-up transition. // On L MR1 devices, we a custom version of the slide-up transition which // doesn't have the delay present in the device default. @@ -2941,7 +2935,7 @@ public class Launcher extends Activity return false; } - @Thunk boolean startActivitySafely(View v, Intent intent, Object tag) { + public boolean startActivitySafely(View v, Intent intent, Object tag) { boolean success = false; if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) { Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show(); @@ -3027,7 +3021,7 @@ public class Launcher extends Activity ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(mFolderIconImageView, alpha, scaleX, scaleY); - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { oa.setInterpolator(new LogDecelerateInterpolator(100, 0)); } oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); @@ -3265,14 +3259,6 @@ public class Launcher extends Activity } } - @Override - public void onStateTransitionHideSearchBar() { - // Hide the search bar - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.hideSearchBar(false /* animated */); - } - } - public void showWorkspace(boolean animated) { showWorkspace(WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null); } @@ -3290,16 +3276,9 @@ public class Launcher extends Activity boolean changed = mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL; if (changed) { - boolean wasInSpringLoadedMode = (mState != State.WORKSPACE); mWorkspace.setVisibility(View.VISIBLE); - mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.NORMAL, - snapToPage, animated, onCompleteRunnable); - - // Show the search bar (only animate if we were showing the drop target bar in spring - // loaded mode) - if (mSearchDropTargetBar != null) { - mSearchDropTargetBar.showSearchBar(animated && wasInSpringLoadedMode); - } + mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), + Workspace.State.NORMAL, snapToPage, animated, onCompleteRunnable); // Set focus to the AppsCustomize button if (mAllAppsButton != null) { @@ -3323,7 +3302,8 @@ public class Launcher extends Activity void showOverviewMode(boolean animated) { mWorkspace.setVisibility(View.VISIBLE); - mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.OVERVIEW, + mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), + Workspace.State.OVERVIEW, WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, animated, null /* onCompleteRunnable */); mState = State.WORKSPACE; @@ -3378,9 +3358,10 @@ public class Launcher extends Activity } if (toState == State.APPS) { - mStateTransitionAnimation.startAnimationToAllApps(animated, focusSearchBar); + mStateTransitionAnimation.startAnimationToAllApps(mWorkspace.getState(), animated, + focusSearchBar); } else { - mStateTransitionAnimation.startAnimationToWidgets(animated); + mStateTransitionAnimation.startAnimationToWidgets(mWorkspace.getState(), animated); } // Change the state *after* we've called all the transition code @@ -3402,10 +3383,9 @@ public class Launcher extends Activity * new state. */ public Animator startWorkspaceStateChangeAnimation(Workspace.State toState, int toPage, - boolean animated, boolean hasOverlaySearchBar, HashMap<View, Integer> layerViews) { + boolean animated, HashMap<View, Integer> layerViews) { Workspace.State fromState = mWorkspace.getState(); - Animator anim = mWorkspace.setStateWithAnimation(toState, toPage, animated, - hasOverlaySearchBar, layerViews); + Animator anim = mWorkspace.setStateWithAnimation(toState, toPage, animated, layerViews); updateInteraction(fromState, toState); return anim; } @@ -3417,7 +3397,8 @@ public class Launcher extends Activity return; } - mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.SPRING_LOADED, + mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), + Workspace.State.SPRING_LOADED, WorkspaceStateTransitionAnimation.SCROLL_TO_CURRENT_PAGE, true /* animated */, null /* onCompleteRunnable */); mState = isAppsViewVisible() ? State.APPS_SPRING_LOADED : State.WIDGETS_SPRING_LOADED; @@ -3526,6 +3507,7 @@ public class Launcher extends Activity mAppWidgetHost.setQsbWidgetId(widgetId); if (widgetId != -1) { mQsb = mAppWidgetHost.createView(this, widgetId, searchProvider); + mQsb.setId(R.id.qsb_widget); mQsb.updateAppWidgetOptions(opts); mQsb.setPadding(0, 0, 0, 0); mSearchDropTargetBar.addView(mQsb); @@ -3782,11 +3764,12 @@ public class Launcher extends Activity continue; } + final View view; switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: ShortcutInfo info = (ShortcutInfo) item; - View shortcut = createShortcut(info); + view = createShortcut(info); /* * TODO: FIX collision case @@ -3805,28 +3788,26 @@ public class Launcher extends Activity } } } - - workspace.addInScreenFromBind(shortcut, item.container, item.screenId, item.cellX, - item.cellY, 1, 1); - if (animateIcons) { - // Animate all the applications up now - shortcut.setAlpha(0f); - shortcut.setScaleX(0f); - shortcut.setScaleY(0f); - bounceAnims.add(createNewAppBounceAnimation(shortcut, i)); - newShortcutsScreenId = item.screenId; - } break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: - FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, + view = FolderIcon.fromXml(R.layout.folder_icon, this, (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item, mIconCache); - workspace.addInScreenFromBind(newFolder, item.container, item.screenId, item.cellX, - item.cellY, 1, 1); break; default: throw new RuntimeException("Invalid Item Type"); } + + workspace.addInScreenFromBind(view, item.container, item.screenId, item.cellX, + item.cellY, 1, 1); + if (animateIcons) { + // Animate all the applications up now + view.setAlpha(0f); + view.setScaleX(0f); + view.setScaleY(0f); + bounceAnims.add(createNewAppBounceAnimation(view, i)); + newShortcutsScreenId = item.screenId; + } } if (animateIcons) { @@ -3901,7 +3882,8 @@ public class Launcher extends Activity if (!mIsSafeModeEnabled && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) == 0) - && ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0)) { + && (item.restoreStatus != LauncherAppWidgetInfo.RESTORE_COMPLETED)) { + if (appWidgetInfo == null) { if (DEBUG_WIDGETS) { Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId @@ -3911,42 +3893,51 @@ public class Launcher extends Activity LauncherModel.deleteItemFromDatabase(this, item); return; } - // Note: This assumes that the id remap broadcast is received before this step. - // If that is not the case, the id remap will be ignored and user may see the - // click to setup view. - PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo, null); - pendingInfo.spanX = item.spanX; - pendingInfo.spanY = item.spanY; - pendingInfo.minSpanX = item.minSpanX; - pendingInfo.minSpanY = item.minSpanY; - Bundle options = null; - WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); - - int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); - boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( - newWidgetId, appWidgetInfo, options); - // TODO consider showing a permission dialog when the widget is clicked. - if (!success) { - mAppWidgetHost.deleteAppWidgetId(newWidgetId); - if (DEBUG_WIDGETS) { - Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId - + " belongs to component " + item.providerName - + ", as the launcher is unable to bing a new widget id"); + // If we do not have a valid id, try to bind an id. + if ((item.restoreStatus & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID) != 0) { + // Note: This assumes that the id remap broadcast is received before this step. + // If that is not the case, the id remap will be ignored and user may see the + // click to setup view. + PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo, null); + pendingInfo.spanX = item.spanX; + pendingInfo.spanY = item.spanY; + pendingInfo.minSpanX = item.minSpanX; + pendingInfo.minSpanY = item.minSpanY; + Bundle options = null; + WidgetHostViewLoader.getDefaultOptionsForWidget(this, pendingInfo); + + int newWidgetId = mAppWidgetHost.allocateAppWidgetId(); + boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed( + newWidgetId, appWidgetInfo, options); + + // TODO consider showing a permission dialog when the widget is clicked. + if (!success) { + mAppWidgetHost.deleteAppWidgetId(newWidgetId); + if (DEBUG_WIDGETS) { + Log.d(TAG, "Removing restored widget: id=" + item.appWidgetId + + " belongs to component " + item.providerName + + ", as the launcher is unable to bing a new widget id"); + } + LauncherModel.deleteItemFromDatabase(this, item); + return; } - LauncherModel.deleteItemFromDatabase(this, item); - return; - } - item.appWidgetId = newWidgetId; + item.appWidgetId = newWidgetId; - // If the widget has a configure activity, it is still needs to set it up, otherwise - // the widget is ready to go. - item.restoreStatus = (appWidgetInfo.configure == null) - ? LauncherAppWidgetInfo.RESTORE_COMPLETED - : LauncherAppWidgetInfo.FLAG_UI_NOT_READY; + // If the widget has a configure activity, it is still needs to set it up, otherwise + // the widget is ready to go. + item.restoreStatus = (appWidgetInfo.configure == null) + ? LauncherAppWidgetInfo.RESTORE_COMPLETED + : LauncherAppWidgetInfo.FLAG_UI_NOT_READY; - LauncherModel.updateItemInDatabase(this, item); + LauncherModel.updateItemInDatabase(this, item); + } else if (((item.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) + && (appWidgetInfo.configure == null)) { + // If the ID is already valid, verify if we need to configure or not. + item.restoreStatus = LauncherAppWidgetInfo.RESTORE_COMPLETED; + LauncherModel.updateItemInDatabase(this, item); + } } if (!mIsSafeModeEnabled && item.restoreStatus == LauncherAppWidgetInfo.RESTORE_COMPLETED) { @@ -3957,6 +3948,8 @@ public class Launcher extends Activity } item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); + item.minSpanX = appWidgetInfo.minSpanX; + item.minSpanY = appWidgetInfo.minSpanY; } else { appWidgetInfo = null; PendingAppWidgetHostView view = new PendingAppWidgetHostView(this, item, @@ -4079,7 +4072,8 @@ public class Launcher extends Activity private boolean canRunNewAppsAnimation() { long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime(); - return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000); + return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000) + && (mClings == null || !mClings.isVisible()); } private ValueAnimator createNewAppBounceAnimation(View v, int i) { @@ -4101,7 +4095,7 @@ public class Launcher extends Activity return mDeviceProfile.getSearchBarBounds(Utilities.isRtl(getResources())); } - public void bindSearchablesChanged() { + public void bindSearchProviderChanged() { if (mSearchDropTargetBar == null) { return; } @@ -4324,13 +4318,14 @@ public class Launcher extends Activity return oriMap[(d.getRotation() + indexOffset) % 4]; } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public void lockScreenOrientation() { if (mRotationEnabled) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (Utilities.ATLEAST_JB_MR2) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + } else { setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources() .getConfiguration().orientation)); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); } } } @@ -4505,6 +4500,7 @@ public class Launcher extends Activity // launcher2). Otherwise, we prompt the user upon started for migration LauncherClings launcherClings = new LauncherClings(this); if (launcherClings.shouldShowFirstRunOrMigrationClings()) { + mClings = launcherClings; if (mModel.canMigrateFromOldLauncherDb(this)) { launcherClings.showMigrationCling(); } else { @@ -4517,14 +4513,16 @@ public class Launcher extends Activity if (mWorkspace != null) mWorkspace.setAlpha(1f); if (mHotseat != null) mHotseat.setAlpha(1f); if (mPageIndicators != null) mPageIndicators.setAlpha(1f); - if (mSearchDropTargetBar != null) mSearchDropTargetBar.showSearchBar(false); + if (mSearchDropTargetBar != null) mSearchDropTargetBar.animateToState( + SearchDropTargetBar.State.SEARCH_BAR, 0); } void hideWorkspaceSearchAndHotseat() { if (mWorkspace != null) mWorkspace.setAlpha(0f); if (mHotseat != null) mHotseat.setAlpha(0f); if (mPageIndicators != null) mPageIndicators.setAlpha(0f); - if (mSearchDropTargetBar != null) mSearchDropTargetBar.hideSearchBar(false); + if (mSearchDropTargetBar != null) mSearchDropTargetBar.animateToState( + SearchDropTargetBar.State.INVISIBLE, 0); } // TODO: These method should be a part of LauncherSearchCallback @@ -4532,7 +4530,7 @@ public class Launcher extends Activity public ItemInfo createAppDragInfo(Intent appLaunchIntent) { // Called from search suggestion UserHandleCompat user = null; - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { UserHandle userHandle = appLaunchIntent.getParcelableExtra(Intent.EXTRA_USER); if (userHandle != null) { user = UserHandleCompat.fromUser(userHandle); diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 0b7b1fdc4..d87ad67e5 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -17,15 +17,16 @@ package com.android.launcher3; import android.app.SearchManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.UserManager; import android.util.Log; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.Thunk; import java.lang.ref.WeakReference; @@ -96,12 +97,12 @@ public class LauncherAppState { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_LOCALE_CHANGED); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); - filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); // For handling managed profiles filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED); filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED); sContext.registerReceiver(mModel, filter); + UserManagerCompat.getInstance(sContext).enableAndResetCache(); } /** @@ -126,7 +127,7 @@ public class LauncherAppState { LauncherModel setLauncher(Launcher launcher) { getLauncherProvider().setLauncherProviderChangeListener(launcher); mModel.initialize(launcher); - mAccessibilityDelegate = ((launcher != null) && Utilities.isLmpOrAbove()) ? + mAccessibilityDelegate = ((launcher != null) && Utilities.ATLEAST_LOLLIPOP) ? new LauncherAccessibilityDelegate(launcher) : null; return mModel; } @@ -147,7 +148,7 @@ public class LauncherAppState { sLauncherProvider = new WeakReference<LauncherProvider>(provider); } - static LauncherProvider getLauncherProvider() { + public static LauncherProvider getLauncherProvider() { return sLauncherProvider.get(); } diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java index de7c61000..6c3a1e896 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHost.java +++ b/src/com/android/launcher3/LauncherAppWidgetHost.java @@ -95,8 +95,6 @@ public class LauncherAppWidgetHost extends AppWidgetHost { } protected void onProvidersChanged() { - mLauncher.getModel().loadAndBindWidgetsAndShortcuts(mLauncher, mLauncher, - true /* refresh */); if (!mProviderChangeListeners.isEmpty()) { for (Runnable callback : new ArrayList<>(mProviderChangeListeners)) { callback.run(); diff --git a/src/com/android/launcher3/LauncherAppWidgetInfo.java b/src/com/android/launcher3/LauncherAppWidgetInfo.java index aad18b578..882f7e202 100644 --- a/src/com/android/launcher3/LauncherAppWidgetInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetInfo.java @@ -68,10 +68,6 @@ public class LauncherAppWidgetInfo extends ItemInfo { ComponentName providerName; - // TODO: Are these necessary here? - int minWidth = -1; - int minHeight = -1; - /** * Indicates the restore status of the widget. */ diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java index 85af92f30..71a08a853 100644 --- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java +++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java @@ -1,10 +1,13 @@ package com.android.launcher3; import android.annotation.TargetApi; +import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Point; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; @@ -19,10 +22,10 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { public boolean isCustomWidget = false; - private int mSpanX = -1; - private int mSpanY = -1; - private int mMinSpanX = -1; - private int mMinSpanY = -1; + public int spanX; + public int spanY; + public int minSpanX; + public int minSpanY; public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context, AppWidgetProviderInfo info) { @@ -41,6 +44,7 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { public LauncherAppWidgetProviderInfo(Parcel in) { super(in); + initSpans(); } public LauncherAppWidgetProviderInfo(Context context, CustomAppWidget widget) { @@ -52,6 +56,41 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { previewImage = widget.getPreviewImage(); initialLayout = widget.getWidgetLayout(); resizeMode = widget.getResizeMode(); + initSpans(); + } + + private void initSpans() { + LauncherAppState app = LauncherAppState.getInstance(); + InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); + + // We only care out the cell size, which is independent of the the layout direction. + Rect paddingLand = idp.landscapeProfile.getWorkspacePadding(false /* isLayoutRtl */); + Rect paddingPort = idp.portraitProfile.getWorkspacePadding(false /* isLayoutRtl */); + + // Always assume we're working with the smallest span to make sure we + // reserve enough space in both orientations. + float smallestCellWidth = DeviceProfile.calculateCellWidth(Math.min( + idp.landscapeProfile.widthPx - paddingLand.left - paddingLand.right, + idp.portraitProfile.widthPx - paddingPort.left - paddingPort.right), + idp.numColumns); + float smallestCellHeight = DeviceProfile.calculateCellWidth(Math.min( + idp.landscapeProfile.heightPx - paddingLand.top - paddingLand.bottom, + idp.portraitProfile.heightPx - paddingPort.top - paddingPort.bottom), + idp.numRows); + + // We want to account for the extra amount of padding that we are adding to the widget + // to ensure that it gets the full amount of space that it has requested. + Rect widgetPadding = AppWidgetHostView.getDefaultPaddingForWidget( + app.getContext(), provider, null); + spanX = Math.max(1, (int) Math.ceil( + (minWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth)); + spanY = Math.max(1, (int) Math.ceil( + (minHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight)); + + minSpanX = Math.max(1, (int) Math.ceil( + (minResizeWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth)); + minSpanY = Math.max(1, (int) Math.ceil( + (minResizeHeight + widgetPadding.top + widgetPadding.bottom) / smallestCellHeight)); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -79,35 +118,9 @@ public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo { provider.toString(), provider.getPackageName(), provider.getShortClassName(), getLabel(pm)); } - public int getSpanX(Launcher launcher) { - lazyLoadSpans(launcher); - return mSpanX; - } - - public int getSpanY(Launcher launcher) { - lazyLoadSpans(launcher); - return mSpanY; - } - - public int getMinSpanX(Launcher launcher) { - lazyLoadSpans(launcher); - return mMinSpanX; - } - - public int getMinSpanY(Launcher launcher) { - lazyLoadSpans(launcher); - return mMinSpanY; - } - - private void lazyLoadSpans(Launcher launcher) { - if (mSpanX < 0 || mSpanY < 0 || mMinSpanX < 0 || mMinSpanY < 0) { - int[] minResizeSpan = launcher.getMinSpanForWidget(this); - int[] span = launcher.getSpanForWidget(this); - - mSpanX = span[0]; - mSpanY = span[1]; - mMinSpanX = minResizeSpan[0]; - mMinSpanY = minResizeSpan[1]; - } + public Point getMinSpans(InvariantDeviceProfile idp, Context context) { + return new Point( + (resizeMode & RESIZE_HORIZONTAL) != 0 ? minSpanX : -1, + (resizeMode & RESIZE_VERTICAL) != 0 ? minSpanY : -1); } } diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index a92a889f9..8eb4e6369 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -24,6 +24,8 @@ import android.database.Cursor; import android.os.ParcelFileDescriptor; import android.util.Log; +import com.android.launcher3.model.MigrateFromRestoreTask; + import java.io.IOException; public class LauncherBackupAgentHelper extends BackupAgentHelper { @@ -63,7 +65,7 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { - if (!Utilities.isLmpOrAbove()) { + if (!Utilities.ATLEAST_LOLLIPOP) { // No restore for old devices. Log.i(TAG, "You shall not pass!!!"); Log.d(TAG, "Restore is only supported on devices running Lollipop and above."); @@ -91,11 +93,19 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { LauncherAppState.getLauncherProvider().clearFlagEmptyDbCreated(); LauncherClings.synchonouslyMarkFirstRunClingDismissed(this); - // TODO: Update the backup set to include rank. + // Rank was added in v4. if (mHelper.restoredBackupVersion <= 3) { LauncherAppState.getLauncherProvider().updateFolderItemsRank(); - LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities(); } + + if (MigrateFromRestoreTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) { + MigrateFromRestoreTask.markForMigration(getApplicationContext(), + (int) mHelper.migrationCompatibleProfileData.desktopCols, + (int) mHelper.migrationCompatibleProfileData.desktopRows, + mHelper.widgetSizes); + } + + LauncherAppState.getLauncherProvider().convertShortcutsToLauncherActivities(); } else { if (VERBOSE) Log.v(TAG, "Nothing was restored, clearing DB"); LauncherAppState.getLauncherProvider().createEmptyDB(); diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 8c6fedbdc..2d11d3af1 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -32,6 +32,7 @@ import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.graphics.drawable.Drawable; import android.os.ParcelFileDescriptor; import android.text.TextUtils; @@ -51,6 +52,7 @@ import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.MigrateFromRestoreTask; import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; @@ -75,7 +77,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; private static final boolean DEBUG = LauncherBackupAgentHelper.DEBUG; - private static final int BACKUP_VERSION = 3; + private static final int BACKUP_VERSION = 4; private static final int MAX_JOURNAL_SIZE = 1000000; // Journal key is such that it is always smaller than any dynamically generated @@ -107,6 +109,7 @@ public class LauncherBackupHelper implements BackupHelper { Favorites.SPANY, // 15 Favorites.TITLE, // 16 Favorites.PROFILE_ID, // 17 + Favorites.RANK, // 18 }; private static final int ID_INDEX = 0; @@ -126,6 +129,7 @@ public class LauncherBackupHelper implements BackupHelper { private static final int SPANX_INDEX = 14; private static final int SPANY_INDEX = 15; private static final int TITLE_INDEX = 16; + private static final int RANK_INDEX = 18; private static final String[] SCREEN_PROJECTION = { WorkspaceScreens._ID, // 0 @@ -150,9 +154,16 @@ public class LauncherBackupHelper implements BackupHelper { private DeviceProfieData mDeviceProfileData; private InvariantDeviceProfile mIdp; + DeviceProfieData migrationCompatibleProfileData; + HashSet<String> widgetSizes = new HashSet<>(); + boolean restoreSuccessful; int restoredBackupVersion = 1; + // When migrating from a device which different hotseat configuration, the icons are shifted + // to center along the new all-apps icon. + private int mHotseatShift = 0; + public LauncherBackupHelper(Context context) { mContext = context; mExistingKeys = new HashSet<String>(); @@ -282,17 +293,39 @@ public class LauncherBackupHelper implements BackupHelper { return true; } - boolean isHotsetCompatible = false; + boolean isHotseatCompatible = false; if (currentProfile.allappsRank >= oldProfile.hotseatCount) { - isHotsetCompatible = true; + isHotseatCompatible = true; + mHotseatShift = 0; + } + + if ((currentProfile.allappsRank >= oldProfile.allappsRank) + && ((currentProfile.hotseatCount - currentProfile.allappsRank) >= + (oldProfile.hotseatCount - oldProfile.allappsRank))) { + // There is enough space on both sides of the hotseat. + isHotseatCompatible = true; + mHotseatShift = currentProfile.allappsRank - oldProfile.allappsRank; + } + + if (!isHotseatCompatible) { + return false; } - if ((currentProfile.hotseatCount >= oldProfile.hotseatCount) && - (currentProfile.allappsRank == oldProfile.allappsRank)) { - isHotsetCompatible = true; + if ((currentProfile.desktopCols >= oldProfile.desktopCols) + && (currentProfile.desktopRows >= oldProfile.desktopRows)) { + return true; } - return isHotsetCompatible && (currentProfile.desktopCols >= oldProfile.desktopCols) - && (currentProfile.desktopRows >= oldProfile.desktopRows); + if (MigrateFromRestoreTask.ENABLED && + (oldProfile.desktopCols - currentProfile.desktopCols <= 1) && + (oldProfile.desktopRows - currentProfile.desktopRows <= 1)) { + // Allow desktop migration when row and/or column count contracts by 1. + + migrationCompatibleProfileData = initDeviceProfileData(mIdp); + migrationCompatibleProfileData.desktopCols = oldProfile.desktopCols; + migrationCompatibleProfileData.desktopRows = oldProfile.desktopRows; + return true; + } + return false; } /** @@ -432,7 +465,10 @@ public class LauncherBackupHelper implements BackupHelper { Key key = getKey(Key.FAVORITE, id); mKeys.add(key); final String backupKey = keyToBackupKey(key); - if (!mExistingKeys.contains(backupKey) || updateTime >= mLastBackupRestoreTime) { + + // Favorite proto changed in v4. Backup again if the version is old. + if (!mExistingKeys.contains(backupKey) || updateTime >= mLastBackupRestoreTime + || restoredBackupVersion < 4) { writeRowToBackup(key, packFavorite(cursor), data); } else { if (DEBUG) Log.d(TAG, "favorite already backup up: " + id); @@ -601,10 +637,11 @@ public class LauncherBackupHelper implements BackupHelper { Bitmap icon = BitmapFactory.decodeByteArray(res.data, 0, res.data.length); if (icon == null) { Log.w(TAG, "failed to unpack icon for " + key.name); + } else { + if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); + mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, + "" /* label */, mUserSerial, mIdp); } - if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name); - mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi, - "" /* label */, mUserSerial); } /** @@ -638,7 +675,9 @@ public class LauncherBackupHelper implements BackupHelper { } else { Log.w(TAG, "empty intent on appwidget: " + id); } - if (mExistingKeys.contains(backupKey) && restoredBackupVersion >= BACKUP_VERSION) { + + // Widget backup proto changed in v3. So add it again if the original backup is old. + if (mExistingKeys.contains(backupKey) && restoredBackupVersion >= 3) { if (DEBUG) Log.d(TAG, "already saved widget " + backupKey); // remember that we already backed this up previously @@ -685,11 +724,12 @@ public class LauncherBackupHelper implements BackupHelper { Log.w(TAG, "failed to unpack widget icon for " + key.name); } else { mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider), - icon, widget.icon.dpi, widget.label, mUserSerial); + icon, widget.icon.dpi, widget.label, mUserSerial, mIdp); } } - // future site of widget table mutation + // Cache widget min sizes incase migration is required. + widgetSizes.add(widget.provider + "#" + widget.minSpanX + "," + widget.minSpanY); } /** create a new key, with an integer ID. @@ -773,6 +813,7 @@ public class LauncherBackupHelper implements BackupHelper { favorite.spanX = c.getInt(SPANX_INDEX); favorite.spanY = c.getInt(SPANY_INDEX); favorite.iconType = c.getInt(ICON_TYPE_INDEX); + favorite.rank = c.getInt(RANK_INDEX); String title = c.getString(TITLE_INDEX); if (!TextUtils.isEmpty(title)) { @@ -847,6 +888,11 @@ public class LauncherBackupHelper implements BackupHelper { throws IOException { Favorite favorite = unpackProto(new Favorite(), buffer, dataSize); + // If it is a hotseat item, move it accordingly. + if (favorite.container == Favorites.CONTAINER_HOTSEAT) { + favorite.screen += mHotseatShift; + } + ContentValues values = new ContentValues(); values.put(Favorites._ID, favorite.id); values.put(Favorites.SCREEN, favorite.screen); @@ -855,6 +901,7 @@ public class LauncherBackupHelper implements BackupHelper { values.put(Favorites.CELLY, favorite.cellY); values.put(Favorites.SPANX, favorite.spanX); values.put(Favorites.SPANY, favorite.spanY); + values.put(Favorites.RANK, favorite.rank); if (favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT) { values.put(Favorites.ICON_TYPE, favorite.iconType); @@ -880,7 +927,11 @@ public class LauncherBackupHelper implements BackupHelper { UserManagerCompat.getInstance(mContext).getSerialNumberForUser(myUserHandle); values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber); - DeviceProfieData currentProfile = mDeviceProfileData; + // If we will attempt grid resize, use the original profile to validate grid size, as + // anything which fits in the original grid should fit in the current grid after + // grid migration. + DeviceProfieData currentProfile = migrationCompatibleProfileData == null + ? mDeviceProfileData : migrationCompatibleProfileData; if (favorite.itemType == Favorites.ITEM_TYPE_APPWIDGET) { if (!TextUtils.isEmpty(favorite.appWidgetProvider)) { @@ -972,14 +1023,9 @@ public class LauncherBackupHelper implements BackupHelper { widget.icon.dpi = dpi; } - // Calculate the spans corresponding to any one of the orientations as it should not change - // based on orientation. - int[] minSpans = CellLayout.rectToCell( - mIdp.portraitProfile, mContext, info.minResizeWidth, info.minResizeHeight, null); - widget.minSpanX = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0 - ? minSpans[0] : -1; - widget.minSpanY = (info.resizeMode & LauncherAppWidgetProviderInfo.RESIZE_VERTICAL) != 0 - ? minSpans[1] : -1; + Point spans = info.getMinSpans(mIdp, mContext); + widget.minSpanX = spans.x; + widget.minSpanY = spans.y; return widget; } @@ -1164,6 +1210,10 @@ public class LauncherBackupHelper implements BackupHelper { } } + public boolean shouldAttemptWorkspaceMigration() { + return migrationCompatibleProfileData != null; + } + /** * A class to check if an activity can handle one of the intents from a list of * predefined intents. diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index 6618cca78..e34bd57fd 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -77,6 +77,7 @@ public interface LauncherCallbacks { public boolean providesSearch(); public boolean startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, Rect sourceBounds); + public boolean startSearchFromAllApps(String query); @Deprecated public void startVoice(); public boolean hasCustomContentToLeft(); diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java index c13752cb1..18fe8ef86 100644 --- a/src/com/android/launcher3/LauncherClings.java +++ b/src/com/android/launcher3/LauncherClings.java @@ -16,8 +16,6 @@ package com.android.launcher3; -import android.accounts.Account; -import android.accounts.AccountManager; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.TargetApi; @@ -36,6 +34,7 @@ import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.accessibility.AccessibilityManager; + import com.android.launcher3.util.Thunk; class LauncherClings implements OnClickListener { @@ -44,8 +43,6 @@ class LauncherClings implements OnClickListener { private static final String TAG_CROP_TOP_AND_SIDES = "crop_bg_top_and_sides"; - private static final boolean DISABLE_CLINGS = false; - private static final int SHOW_CLING_DURATION = 250; private static final int DISMISS_CLING_DURATION = 200; @@ -54,6 +51,7 @@ class LauncherClings implements OnClickListener { @Thunk Launcher mLauncher; private LayoutInflater mInflater; + @Thunk boolean mIsVisible; /** Ctor */ public LauncherClings(Launcher launcher) { @@ -94,6 +92,7 @@ class LauncherClings implements OnClickListener { * package was not preinstalled and there exists a db to migrate from. */ public void showMigrationCling() { + mIsVisible = true; mLauncher.hideWorkspaceSearchAndHotseat(); ViewGroup root = (ViewGroup) mLauncher.findViewById(R.id.launcher); @@ -120,6 +119,7 @@ class LauncherClings implements OnClickListener { } public void showLongPressCling(boolean showWelcome) { + mIsVisible = true; ViewGroup root = (ViewGroup) mLauncher.findViewById(R.id.launcher); View cling = mInflater.inflate(R.layout.longpress_cling, root, false); @@ -199,6 +199,7 @@ class LauncherClings implements OnClickListener { mLauncher.getSharedPrefs().edit() .putBoolean(flag, true) .apply(); + mIsVisible = false; if (postAnimationCb != null) { postAnimationCb.run(); } @@ -212,13 +213,13 @@ class LauncherClings implements OnClickListener { } } + public boolean isVisible() { + return mIsVisible; + } + /** Returns whether the clings are enabled or should be shown */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private boolean areClingsEnabled() { - if (DISABLE_CLINGS) { - return false; - } - // disable clings when running in a test harness if(ActivityManager.isRunningInTestHarness()) return false; @@ -231,10 +232,7 @@ class LauncherClings implements OnClickListener { // Restricted secondary users (child mode) will potentially have very few apps // seeded when they start up for the first time. Clings won't work well with that - boolean supportsLimitedUsers = - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; - Account[] accounts = AccountManager.get(mLauncher).getAccounts(); - if (supportsLimitedUsers && accounts.length == 0) { + if (Utilities.ATLEAST_JB_MR2) { UserManager um = (UserManager) mLauncher.getSystemService(Context.USER_SERVICE); Bundle restrictions = um.getUserRestrictions(); if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index b60477fa0..b5922c6a3 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -17,6 +17,7 @@ package com.android.launcher3; import android.app.SearchManager; +import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -33,7 +34,6 @@ import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; -import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; @@ -55,6 +55,7 @@ import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.model.MigrateFromRestoreTask; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.CursorIconInfo; @@ -203,7 +204,7 @@ public class LauncherModel extends BroadcastReceiver public void bindComponentsRemoved(ArrayList<String> packageNames, ArrayList<AppInfo> appInfos, UserHandleCompat user, int reason); public void bindAllPackages(WidgetsModel model); - public void bindSearchablesChanged(); + public void bindSearchProviderChanged(); public boolean isAllAppsButtonRank(int rank); public void onPageBoundSynchronously(int page); public void dumpLogsToLocalData(); @@ -257,7 +258,7 @@ public class LauncherModel extends BroadcastReceiver /** Runs the specified runnable immediately if called from the worker thread, otherwise it is * posted on the worker thread handler. */ - private static void runOnWorkerThread(Runnable r) { + @Thunk static void runOnWorkerThread(Runnable r) { if (sWorkerThread.getThreadId() == Process.myTid()) { r.run(); } else { @@ -266,19 +267,6 @@ public class LauncherModel extends BroadcastReceiver } } - /** - * Runs the specified runnable after the loader is complete - */ - @Thunk void runAfterBindCompletes(Runnable r) { - if (isLoadingWorkspace() || !mHasLoaderCompletedOnce) { - synchronized (mBindCompleteRunnables) { - mBindCompleteRunnables.add(r); - } - } else { - runOnWorkerThread(r); - } - } - boolean canMigrateFromOldLauncherDb(Launcher launcher) { return mOldContentProviderExists && !launcher.isLauncherPreinstalled() ; } @@ -892,8 +880,13 @@ public class LauncherModel extends BroadcastReceiver } private void assertWorkspaceLoaded() { - if (LauncherAppState.isDogfoodBuild() && (isLoadingWorkspace() || !mHasLoaderCompletedOnce)) { - throw new RuntimeException("Trying to add shortcut while loader is running"); + if (LauncherAppState.isDogfoodBuild()) { + synchronized (mLock) { + if (!mHasLoaderCompletedOnce || + (mLoaderTask != null && mLoaderTask.mIsLoadingAndBindingWorkspace)) { + throw new RuntimeException("Trying to add shortcut while loader is running"); + } + } } } @@ -1133,7 +1126,7 @@ public class LauncherModel extends BroadcastReceiver * Update the order of the workspace screens in the database. The array list contains * a list of screen ids in the order that they should appear. */ - void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) { + public void updateWorkspaceScreenOrder(Context context, final ArrayList<Long> screens) { final ArrayList<Long> screensCopy = new ArrayList<Long>(screens); final ContentResolver cr = context.getContentResolver(); final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -1280,14 +1273,14 @@ public class LauncherModel extends BroadcastReceiver if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { // If we have changed locale we need to clear out the labels in all apps/workspace. forceReload(); - } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) || - SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) { + } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action)) { Callbacks callbacks = getCallback(); if (callbacks != null) { - callbacks.bindSearchablesChanged(); + callbacks.bindSearchProviderChanged(); } } else if (LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED.equals(action) || LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { + UserManagerCompat.getInstance(context).enableAndResetCache(); forceReload(); } } @@ -1388,16 +1381,6 @@ public class LauncherModel extends BroadcastReceiver mHandler.post(r); } } - - // Run all the bind complete runnables after workspace is bound. - if (!mBindCompleteRunnables.isEmpty()) { - synchronized (mBindCompleteRunnables) { - for (final Runnable r : mBindCompleteRunnables) { - runOnWorkerThread(r); - } - mBindCompleteRunnables.clear(); - } - } } public void stopLoader() { @@ -1411,7 +1394,7 @@ public class LauncherModel extends BroadcastReceiver /** * Loads the workspace screen ids in an ordered list. */ - @Thunk static ArrayList<Long> loadWorkspaceScreensDb(Context context) { + public static ArrayList<Long> loadWorkspaceScreensDb(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -1439,15 +1422,6 @@ public class LauncherModel extends BroadcastReceiver return mAllAppsLoaded; } - boolean isLoadingWorkspace() { - synchronized (mLock) { - if (mLoaderTask != null) { - return mLoaderTask.isLoadingWorkspace(); - } - } - return false; - } - /** * Runnable for the thread that loads the contents of the launcher: * - workspace icons @@ -1466,10 +1440,6 @@ public class LauncherModel extends BroadcastReceiver mFlags = flags; } - boolean isLoadingWorkspace() { - return mIsLoadingAndBindingWorkspace; - } - private void loadAndBindWorkspace() { mIsLoadingAndBindingWorkspace = true; @@ -1641,11 +1611,12 @@ public class LauncherModel extends BroadcastReceiver } // check & update map of what's occupied; used to discard overlapping/invalid items - private boolean checkItemPlacement(LongArrayMap<ItemInfo[][]> occupied, ItemInfo item) { + private boolean checkItemPlacement(LongArrayMap<ItemInfo[][]> occupied, ItemInfo item, + ArrayList<Long> workspaceScreens) { LauncherAppState app = LauncherAppState.getInstance(); InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); - final int countX = (int) profile.numColumns; - final int countY = (int) profile.numRows; + final int countX = profile.numColumns; + final int countY = profile.numRows; long containerIndex = item.screenId; if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { @@ -1687,7 +1658,12 @@ public class LauncherModel extends BroadcastReceiver occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items); return true; } - } else if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { + } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + if (!workspaceScreens.contains((Long) item.screenId)) { + // The item has an invalid screen id. + return false; + } + } else { // Skip further checking if it is not the hotseat or workspace container return true; } @@ -1754,8 +1730,27 @@ public class LauncherModel extends BroadcastReceiver LauncherAppState app = LauncherAppState.getInstance(); InvariantDeviceProfile profile = app.getInvariantDeviceProfile(); - int countX = (int) profile.numColumns; - int countY = (int) profile.numRows; + int countX = profile.numColumns; + int countY = profile.numRows; + + if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) { + long migrationStartTime = System.currentTimeMillis(); + Log.v(TAG, "Starting workspace migration after restore"); + try { + MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext); + // Clear the flags before starting the task, so that we do not run the task + // again, in case there was an uncaught error. + MigrateFromRestoreTask.clearFlags(mContext); + task.execute(); + } catch (Exception e) { + Log.e(TAG, "Error during grid migration", e); + + // Clear workspace. + mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE; + } + Log.v(TAG, "Workspace migration completed in " + + (System.currentTimeMillis() - migrationStartTime)); + } if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) { Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true); @@ -1776,6 +1771,7 @@ public class LauncherModel extends BroadcastReceiver clearSBgDataStructures(); final HashMap<String, Integer> installingPkgs = PackageInstallerCompat .getInstance(mContext).updateAndGetActiveSessionCache(); + sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final ArrayList<Long> restoredRows = new ArrayList<Long>(); @@ -1977,6 +1973,7 @@ public class LauncherModel extends BroadcastReceiver } catch (URISyntaxException e) { Launcher.addDumpLog(TAG, "Invalid uri: " + intentDescription, true); + itemsToRemove.add(id); continue; } @@ -2047,7 +2044,7 @@ public class LauncherModel extends BroadcastReceiver } // check & update map of what's occupied - if (!checkItemPlacement(occupied, info)) { + if (!checkItemPlacement(occupied, info, sBgWorkspaceScreens)) { itemsToRemove.add(id); break; } @@ -2098,7 +2095,7 @@ public class LauncherModel extends BroadcastReceiver folderInfo.options = c.getInt(optionsIndex); // check & update map of what's occupied - if (!checkItemPlacement(occupied, folderInfo)) { + if (!checkItemPlacement(occupied, folderInfo, sBgWorkspaceScreens)) { itemsToRemove.add(id); break; } @@ -2163,14 +2160,18 @@ public class LauncherModel extends BroadcastReceiver appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, provider.provider); - int status = restoreStatus; + // The provider is available. So the widget is either + // available or not available. We do not need to track + // any future restore updates. + int status = restoreStatus & + ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; if (!wasProviderReady) { // If provider was not previously ready, update the // status and UI flag. // Id would be valid only if the widget restore broadcast was received. if (isIdValid) { - status = LauncherAppWidgetInfo.RESTORE_COMPLETED; + status = LauncherAppWidgetInfo.FLAG_UI_NOT_READY; } else { status &= ~LauncherAppWidgetInfo .FLAG_PROVIDER_NOT_READY; @@ -2214,13 +2215,14 @@ public class LauncherModel extends BroadcastReceiver if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP && container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { Log.e(TAG, "Widget found where container != " + - "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!"); + "CONTAINER_DESKTOP nor CONTAINER_HOTSEAT - ignoring!"); + itemsToRemove.add(id); continue; } appWidgetInfo.container = container; // check & update map of what's occupied - if (!checkItemPlacement(occupied, appWidgetInfo)) { + if (!checkItemPlacement(occupied, appWidgetInfo, sBgWorkspaceScreens)) { itemsToRemove.add(id); break; } @@ -2279,6 +2281,21 @@ public class LauncherModel extends BroadcastReceiver } } + // Sort all the folder items and make sure the first 3 items are high resolution. + for (FolderInfo folder : sBgFolders) { + Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); + int pos = 0; + for (ShortcutInfo info : folder.contents) { + if (info.usingLowResIcon) { + info.updateIcon(mIconCache, false); + } + pos ++; + if (pos >= FolderIcon.NUM_ITEMS_IN_PREVIEW) { + break; + } + } + } + if (restoredRows.size() > 0) { // Update restored items that no longer require special handling ContentValues values = new ContentValues(); @@ -2294,8 +2311,6 @@ public class LauncherModel extends BroadcastReceiver null, sWorker); } - sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext)); - // Remove any empty screens ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens); for (ItemInfo item: sBgItemsIdMap) { @@ -2657,13 +2672,24 @@ public class LauncherModel extends BroadcastReceiver callbacks.finishBindingItems(); } + mIsLoadingAndBindingWorkspace = false; + + // Run all the bind complete runnables after workspace is bound. + if (!mBindCompleteRunnables.isEmpty()) { + synchronized (mBindCompleteRunnables) { + for (final Runnable r : mBindCompleteRunnables) { + runOnWorkerThread(r); + } + mBindCompleteRunnables.clear(); + } + } + // If we're profiling, ensure this is the last thing in the queue. if (DEBUG_LOADERS) { Log.d(TAG, "bound workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); } - mIsLoadingAndBindingWorkspace = false; } }; if (isLoadingSynchronously) { @@ -2792,12 +2818,27 @@ public class LauncherModel extends BroadcastReceiver final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user); if (heuristic != null) { - runAfterBindCompletes(new Runnable() { + final Runnable r = new Runnable() { @Override public void run() { heuristic.processUserApps(apps); } + }; + runOnMainThread(new Runnable() { + + @Override + public void run() { + // Check isLoadingWorkspace on the UI thread, as it is updated on + // the UI thread. + if (mIsLoadingAndBindingWorkspace) { + synchronized (mBindCompleteRunnables) { + mBindCompleteRunnables.add(r); + } + } else { + runOnWorkerThread(r); + } + } }); } } @@ -2825,8 +2866,7 @@ public class LauncherModel extends BroadcastReceiver // Cleanup any data stored for a deleted user. ManagedProfileHeuristic.processAllUsers(profiles, mContext); - loadAndBindWidgetsAndShortcuts(mApp.getContext(), tryGetCallbacks(oldCallbacks), - true /* refresh */); + loadAndBindWidgetsAndShortcuts(tryGetCallbacks(oldCallbacks), true /* refresh */); if (DEBUG_LOADERS) { Log.d(TAG, "Icons processed in " + (SystemClock.uptimeMillis() - loadTime) + "ms"); @@ -2895,7 +2935,7 @@ public class LauncherModel extends BroadcastReceiver } // Reload widget list. No need to refresh, as we only want to update the icons and labels. - loadAndBindWidgetsAndShortcuts(mApp.getContext(), callbacks, false); + loadAndBindWidgetsAndShortcuts(callbacks, false); } void enqueuePackageUpdated(PackageUpdatedTask task) { @@ -3155,7 +3195,15 @@ public class LauncherModel extends BroadcastReceiver if (mUser.equals(widgetInfo.user) && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY) && packageSet.contains(widgetInfo.providerName.getPackageName())) { - widgetInfo.restoreStatus &= ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; + widgetInfo.restoreStatus &= + ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY & + ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; + + // adding this flag ensures that launcher shows 'click to setup' + // if the widget has a config activity. In case there is no config + // activity, it will be marked as 'restored' during bind. + widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY; + widgets.add(widgetInfo); updateItemInDatabase(context, widgetInfo); } @@ -3235,8 +3283,36 @@ public class LauncherModel extends BroadcastReceiver }); } - // onProvidersChanged method (API >= 17) already refreshed the widget list - loadAndBindWidgetsAndShortcuts(context, callbacks, Build.VERSION.SDK_INT < 17); + // Update widgets + if (mOp == OP_ADD || mOp == OP_REMOVE || mOp == OP_UPDATE) { + // Always refresh for a package event on secondary user + boolean needToRefresh = !mUser.equals(UserHandleCompat.myUserHandle()); + + // Refresh widget list, if the package already had a widget. + synchronized (sBgLock) { + if (sBgWidgetProviders != null) { + HashSet<String> pkgSet = new HashSet<>(); + Collections.addAll(pkgSet, mPackages); + + for (ComponentKey key : sBgWidgetProviders.keySet()) { + needToRefresh |= key.user.equals(mUser) && + pkgSet.contains(key.componentName.getPackageName()); + } + } + } + + if (!needToRefresh && mOp != OP_REMOVE) { + // Refresh widget list, if there is any newly added widget + PackageManager pm = context.getPackageManager(); + for (String pkg : mPackages) { + needToRefresh |= !pm.queryBroadcastReceivers( + new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE) + .setPackage(pkg), 0).isEmpty(); + } + } + + loadAndBindWidgetsAndShortcuts(callbacks, needToRefresh); + } // Write all the logs to disk mHandler.post(new Runnable() { @@ -3310,13 +3386,12 @@ public class LauncherModel extends BroadcastReceiver } } - public void loadAndBindWidgetsAndShortcuts(final Context context, final Callbacks callbacks, - final boolean refresh) { + public void loadAndBindWidgetsAndShortcuts(final Callbacks callbacks, final boolean refresh) { runOnWorkerThread(new Runnable() { @Override public void run() { - updateWidgetsModel(context, refresh); + updateWidgetsModel(refresh); final WidgetsModel model = mBgWidgetsModel.clone(); mHandler.post(new Runnable() { @@ -3340,10 +3415,10 @@ public class LauncherModel extends BroadcastReceiver * * @see #loadAndBindWidgetsAndShortcuts */ - @Thunk void updateWidgetsModel(Context context, boolean refresh) { - PackageManager packageManager = context.getPackageManager(); + @Thunk void updateWidgetsModel(boolean refresh) { + PackageManager packageManager = mApp.getContext().getPackageManager(); final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>(); - widgetsAndShortcuts.addAll(getWidgetProviders(context, refresh)); + widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh)); Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT); widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0)); mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts); diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index cc5e18bc1..8791e9e57 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -65,20 +65,17 @@ import java.util.HashSet; import java.util.List; public class LauncherProvider extends ContentProvider { - private static final String TAG = "Launcher.LauncherProvider"; + private static final String TAG = "LauncherProvider"; private static final boolean LOGD = false; private static final int DATABASE_VERSION = 26; - static final String OLD_AUTHORITY = "com.android.launcher2.settings"; - static final String AUTHORITY = ProviderConfig.AUTHORITY; + public static final String AUTHORITY = ProviderConfig.AUTHORITY; static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME; static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME; static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; - private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd"; - private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name"; @Thunk LauncherProviderChangeListener mListener; @@ -140,14 +137,21 @@ public class LauncherProvider extends ContentProvider { return db.insert(table, nullColumnHack, values); } + private void reloadLauncherIfExternal() { + if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app != null) { + app.reloadWorkspace(); + } + } + } + @Override public Uri insert(Uri uri, ContentValues initialValues) { SqlArguments args = new SqlArguments(uri); - // In very limited cases, we support system|signature permission apps to add to the db - String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD); - final boolean isExternalAll = externalAdd != null && "true".equals(externalAdd); - if (isExternalAll) { + // In very limited cases, we support system|signature permission apps to modify the db. + if (Binder.getCallingPid() != Process.myPid()) { if (!mOpenHelper.initializeExternalAdd(initialValues)) { return null; } @@ -161,13 +165,20 @@ public class LauncherProvider extends ContentProvider { uri = ContentUris.withAppendedId(uri, rowId); notifyListeners(); - if (isExternalAll) { + if (Utilities.ATLEAST_MARSHMALLOW) { + reloadLauncherIfExternal(); + } else { + // Deprecated behavior to support legacy devices which rely on provider callbacks. LauncherAppState app = LauncherAppState.getInstanceNoCreate(); - if (app != null) { + if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) { app.reloadWorkspace(); } - } + String notify = uri.getQueryParameter("notify"); + if (notify == null || "true".equals(notify)) { + getContext().getContentResolver().notifyChange(uri, null); + } + } return uri; } @@ -192,6 +203,7 @@ public class LauncherProvider extends ContentProvider { } notifyListeners(); + reloadLauncherIfExternal(); return values.length; } @@ -203,6 +215,7 @@ public class LauncherProvider extends ContentProvider { try { ContentProviderResult[] result = super.applyBatch(operations); db.setTransactionSuccessful(); + reloadLauncherIfExternal(); return result; } finally { db.endTransaction(); @@ -217,6 +230,7 @@ public class LauncherProvider extends ContentProvider { int count = db.delete(args.table, args.where, args.args); if (count > 0) notifyListeners(); + reloadLauncherIfExternal(); return count; } @@ -229,6 +243,7 @@ public class LauncherProvider extends ContentProvider { int count = db.update(args.table, values, args.where, args.args); if (count > 0) notifyListeners(); + reloadLauncherIfExternal(); return count; } @@ -399,7 +414,7 @@ public class LauncherProvider extends ContentProvider { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() { // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18 - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (!Utilities.ATLEAST_JB_MR2) { return null; } diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index f2c85a195..8a5804f34 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -143,7 +143,7 @@ public class LauncherSettings { * * @return The unique content URL for the specified row. */ - static Uri getContentUri(long id) { + public static Uri getContentUri(long id) { return Uri.parse("content://" + ProviderConfig.AUTHORITY + "/" + TABLE_NAME + "/" + id); } diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java index d69b7432d..cdde8c13f 100644 --- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -80,13 +80,6 @@ import java.util.HashMap; public class LauncherStateTransitionAnimation { /** - * Callbacks made during the state transition - */ - interface Callbacks { - public void onStateTransitionHideSearchBar(); - } - - /** * Private callbacks made during transition setup. */ static abstract class PrivateTransitionCallbacks { @@ -111,12 +104,10 @@ public class LauncherStateTransitionAnimation { public static final int SINGLE_FRAME_DELAY = 16; @Thunk Launcher mLauncher; - @Thunk Callbacks mCb; - @Thunk AnimatorSet mStateAnimation; + @Thunk AnimatorSet mCurrentAnimation; - public LauncherStateTransitionAnimation(Launcher l, Callbacks cb) { + public LauncherStateTransitionAnimation(Launcher l) { mLauncher = l; - mCb = cb; } /** @@ -125,8 +116,8 @@ public class LauncherStateTransitionAnimation { * @param startSearchAfterTransition Immediately starts app search after the transition to * All Apps is completed. */ - public void startAnimationToAllApps(final boolean animated, - final boolean startSearchAfterTransition) { + public void startAnimationToAllApps(final Workspace.State fromWorkspaceState, + final boolean animated, final boolean startSearchAfterTransition) { final AllAppsContainerView toView = mLauncher.getAppsView(); final View buttonView = mLauncher.getAllAppsButton(); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { @@ -159,15 +150,16 @@ public class LauncherStateTransitionAnimation { } }; // Only animate the search bar if animating from spring loaded mode back to all apps - startAnimationToOverlay(Workspace.State.NORMAL_HIDDEN, buttonView, toView, - toView.getContentView(), toView.getRevealView(), toView.getSearchBarView(), - animated, true /* hideSearchBar */, cb); + mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState, + Workspace.State.NORMAL_HIDDEN, buttonView, toView, toView.getContentView(), + toView.getRevealView(), toView.getSearchBarView(), animated, cb); } /** * Starts an animation to the widgets view. */ - public void startAnimationToWidgets(final boolean animated) { + public void startAnimationToWidgets(final Workspace.State fromWorkspaceState, + final boolean animated) { final WidgetsContainerView toView = mLauncher.getWidgetsView(); final View buttonView = mLauncher.getWidgetsButton(); @@ -177,17 +169,17 @@ public class LauncherStateTransitionAnimation { return 0.3f; } }; - startAnimationToOverlay(Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, - toView.getContentView(), toView.getRevealView(), null, animated, - true /* hideSearchBar */, cb); + mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState, + Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, toView.getContentView(), + toView.getRevealView(), null, animated, cb); } /** * Starts and animation to the workspace from the current overlay view. */ public void startAnimationToWorkspace(final Launcher.State fromState, - final Workspace.State toWorkspaceState, final int toWorkspacePage, - final boolean animated, final Runnable onCompleteRunnable) { + final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, + final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { if (toWorkspaceState != Workspace.State.NORMAL && toWorkspaceState != Workspace.State.SPRING_LOADED && toWorkspaceState != Workspace.State.OVERVIEW) { @@ -195,10 +187,10 @@ public class LauncherStateTransitionAnimation { } if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) { - startAnimationToWorkspaceFromAllApps(toWorkspaceState, toWorkspacePage, + startAnimationToWorkspaceFromAllApps(fromWorkspaceState, toWorkspaceState, toWorkspacePage, animated, onCompleteRunnable); } else { - startAnimationToWorkspaceFromWidgets(toWorkspaceState, toWorkspacePage, + startAnimationToWorkspaceFromWidgets(fromWorkspaceState, toWorkspaceState, toWorkspacePage, animated, onCompleteRunnable); } } @@ -207,12 +199,13 @@ public class LauncherStateTransitionAnimation { * Creates and starts a new animation to a particular overlay view. */ @SuppressLint("NewApi") - private void startAnimationToOverlay(final Workspace.State toWorkspaceState, - final View buttonView, final View toView, final View contentView, final View revealView, - final View overlaySearchBarView, final boolean animated, final boolean hideSearchBar, - final PrivateTransitionCallbacks pCb) { + private AnimatorSet startAnimationToOverlay(final Workspace.State fromWorkspaceState, + final Workspace.State toWorkspaceState, final View buttonView, final View toView, + final View contentView, final View revealView, final View overlaySearchBarView, + final boolean animated, final PrivateTransitionCallbacks pCb) { + final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); - final boolean material = Utilities.isLmpOrAbove(); + final boolean material = Utilities.ATLEAST_LOLLIPOP; final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); @@ -230,11 +223,13 @@ public class LauncherStateTransitionAnimation { // Create the workspace animation. // NOTE: this call apparently also sets the state for the workspace if !animated Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, -1, - animated, overlaySearchBarView != null /* hasOverlaySearchBar */, layerViews); + animated, layerViews); - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + // Animate the search bar + startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState, + animated ? revealDuration : 0, overlaySearchBarView); + if (animated && initialized) { // Setup the reveal view animation int width = revealView.getMeasuredWidth(); int height = revealView.getMeasuredHeight(); @@ -274,7 +269,7 @@ public class LauncherStateTransitionAnimation { // Play the animation layerViews.put(revealView, BUILD_AND_SET_LAYER); - mStateAnimation.play(panelAlphaAndDrift); + animation.play(panelAlphaAndDrift); if (overlaySearchBarView != null) { overlaySearchBarView.setAlpha(0f); @@ -282,7 +277,7 @@ public class LauncherStateTransitionAnimation { searchBarAlpha.setDuration(100); searchBarAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); - mStateAnimation.play(searchBarAlpha); + animation.play(searchBarAlpha); } // Setup the animation for the content view @@ -297,13 +292,13 @@ public class LauncherStateTransitionAnimation { pageDrift.setDuration(revealDuration); pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0)); pageDrift.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(pageDrift); + animation.play(pageDrift); ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f); itemsAlpha.setDuration(revealDuration); itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); itemsAlpha.setStartDelay(itemsAlphaStagger); - mStateAnimation.play(itemsAlpha); + animation.play(itemsAlpha); if (material) { float startRadius = pCb.getMaterialRevealViewStartFinalRadius(); @@ -316,10 +311,10 @@ public class LauncherStateTransitionAnimation { if (listener != null) { reveal.addListener(listener); } - mStateAnimation.play(reveal); + animation.play(reveal); } - mStateAnimation.addListener(new AnimatorListenerAdapter() { + animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { dispatchOnLauncherTransitionEnd(fromView, animated, false); @@ -335,12 +330,8 @@ public class LauncherStateTransitionAnimation { } } - if (hideSearchBar) { - mCb.onStateTransitionHideSearchBar(); - } - // This can hold unnecessary references to views. - mStateAnimation = null; + cleanupAnimation(); pCb.onTransitionComplete(); } @@ -348,7 +339,7 @@ public class LauncherStateTransitionAnimation { // Play the workspace animation if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); + animation.play(workspaceAnim); } // Dispatch the prepare transition signal @@ -356,23 +347,22 @@ public class LauncherStateTransitionAnimation { dispatchOnLauncherTransitionPrepare(toView, animated, false); - final AnimatorSet stateAnimation = mStateAnimation; + final AnimatorSet stateAnimation = animation; final Runnable startAnimRunnable = new Runnable() { public void run() { - // Check that mStateAnimation hasn't changed while + // Check that mCurrentAnimation hasn't changed while // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) + if (mCurrentAnimation != stateAnimation) return; dispatchOnLauncherTransitionStart(fromView, animated, false); dispatchOnLauncherTransitionStart(toView, animated, false); // Enable all necessary layers - boolean isLmpOrAbove = Utilities.isLmpOrAbove(); for (View v : layerViews.keySet()) { if (layerViews.get(v) == BUILD_AND_SET_LAYER) { v.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + if (Utilities.ATLEAST_LOLLIPOP && Utilities.isViewAttachedToWindow(v)) { v.buildLayer(); } } @@ -380,12 +370,14 @@ public class LauncherStateTransitionAnimation { // Focus the new view toView.requestFocus(); - mStateAnimation.start(); + stateAnimation.start(); } }; toView.bringToFront(); toView.setVisibility(View.VISIBLE); toView.post(startAnimRunnable); + + return animation; } else { toView.setTranslationX(0.0f); toView.setTranslationY(0.0f); @@ -397,10 +389,6 @@ public class LauncherStateTransitionAnimation { // Show the content view contentView.setVisibility(View.VISIBLE); - if (hideSearchBar) { - mCb.onStateTransitionHideSearchBar(); - } - dispatchOnLauncherTransitionPrepare(fromView, animated, false); dispatchOnLauncherTransitionStart(fromView, animated, false); dispatchOnLauncherTransitionEnd(fromView, animated, false); @@ -408,18 +396,19 @@ public class LauncherStateTransitionAnimation { dispatchOnLauncherTransitionStart(toView, animated, false); dispatchOnLauncherTransitionEnd(toView, animated, false); pCb.onTransitionComplete(); + + return null; } } /** * Starts and animation to the workspace from the apps view. */ - private void startAnimationToWorkspaceFromAllApps(final Workspace.State toWorkspaceState, - final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + private void startAnimationToWorkspaceFromAllApps(final Workspace.State fromWorkspaceState, + final Workspace.State toWorkspaceState, final int toWorkspacePage, + final boolean animated, final Runnable onCompleteRunnable) { AllAppsContainerView appsView = mLauncher.getAppsView(); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { - int[] mAllAppsToPanelDelta; - @Override float getMaterialRevealViewFinalAlpha(View revealView) { // No alpha anim from all apps @@ -451,8 +440,8 @@ public class LauncherStateTransitionAnimation { } }; // Only animate the search bar if animating to spring loaded mode from all apps - startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, - mLauncher.getAllAppsButton(), appsView, appsView.getContentView(), + mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState, + toWorkspacePage, mLauncher.getAllAppsButton(), appsView, appsView.getContentView(), appsView.getRevealView(), appsView.getSearchBarView(), animated, onCompleteRunnable, cb); } @@ -460,8 +449,9 @@ public class LauncherStateTransitionAnimation { /** * Starts and animation to the workspace from the widgets view. */ - private void startAnimationToWorkspaceFromWidgets(final Workspace.State toWorkspaceState, - final int toWorkspacePage, final boolean animated, final Runnable onCompleteRunnable) { + private void startAnimationToWorkspaceFromWidgets(final Workspace.State fromWorkspaceState, + final Workspace.State toWorkspaceState, final int toWorkspacePage, + final boolean animated, final Runnable onCompleteRunnable) { final WidgetsContainerView widgetsView = mLauncher.getWidgetsView(); PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() { @Override @@ -479,21 +469,23 @@ public class LauncherStateTransitionAnimation { }; } }; - startAnimationToWorkspaceFromOverlay(toWorkspaceState, toWorkspacePage, - mLauncher.getWidgetsButton(), widgetsView, widgetsView.getContentView(), - widgetsView.getRevealView(), null, animated, onCompleteRunnable, cb); + mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, + toWorkspaceState, toWorkspacePage, mLauncher.getWidgetsButton(), widgetsView, + widgetsView.getContentView(), widgetsView.getRevealView(), null, animated, + onCompleteRunnable, cb); } /** * Creates and starts a new animation to the workspace. */ - private void startAnimationToWorkspaceFromOverlay(final Workspace.State toWorkspaceState, - final int toWorkspacePage, final View buttonView, final View fromView, - final View contentView, final View revealView, final View overlaySearchBarView, - final boolean animated, final Runnable onCompleteRunnable, - final PrivateTransitionCallbacks pCb) { + private AnimatorSet startAnimationToWorkspaceFromOverlay(final Workspace.State fromWorkspaceState, + final Workspace.State toWorkspaceState, final int toWorkspacePage, final View buttonView, + final View fromView, final View contentView, final View revealView, + final View overlaySearchBarView, final boolean animated, final Runnable onCompleteRunnable, + final PrivateTransitionCallbacks pCb) { + final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); final Resources res = mLauncher.getResources(); - final boolean material = Utilities.isLmpOrAbove(); + final boolean material = Utilities.ATLEAST_LOLLIPOP; final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime); final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger); @@ -511,15 +503,16 @@ public class LauncherStateTransitionAnimation { // Create the workspace animation. // NOTE: this call apparently also sets the state for the workspace if !animated Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState, - toWorkspacePage, animated, overlaySearchBarView != null /* hasOverlaySearchBar */, - layerViews); + toWorkspacePage, animated, layerViews); - if (animated && initialized) { - mStateAnimation = LauncherAnimUtils.createAnimatorSet(); + // Animate the search bar + startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState, + animated ? revealDuration : 0, overlaySearchBarView); + if (animated && initialized) { // Play the workspace animation if (workspaceAnim != null) { - mStateAnimation.play(workspaceAnim); + animation.play(workspaceAnim); } // hideAppsCustomizeHelper is called in some cases when it is already hidden @@ -558,14 +551,14 @@ public class LauncherStateTransitionAnimation { panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY); panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); panelDriftY.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftY); + animation.play(panelDriftY); ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX", 0, revealViewToXDrift); panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY); panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); panelDriftX.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelDriftX); + animation.play(panelDriftX); // Setup animation for the reveal panel alpha final float revealViewToAlpha = !material ? 0f : @@ -576,7 +569,7 @@ public class LauncherStateTransitionAnimation { panelAlpha.setDuration(material ? revealDuration : 150); panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); panelAlpha.setInterpolator(decelerateInterpolator); - mStateAnimation.play(panelAlpha); + animation.play(panelAlpha); } // Setup the animation for the content view @@ -589,13 +582,13 @@ public class LauncherStateTransitionAnimation { pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY); pageDrift.setInterpolator(decelerateInterpolator); pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY); - mStateAnimation.play(pageDrift); + animation.play(pageDrift); contentView.setAlpha(1f); ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f); itemsAlpha.setDuration(100); itemsAlpha.setInterpolator(decelerateInterpolator); - mStateAnimation.play(itemsAlpha); + animation.play(itemsAlpha); if (overlaySearchBarView != null) { overlaySearchBarView.setAlpha(1f); @@ -604,7 +597,7 @@ public class LauncherStateTransitionAnimation { searchAlpha.setInterpolator(decelerateInterpolator); searchAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY); layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER); - mStateAnimation.play(searchAlpha); + animation.play(searchAlpha); } if (material) { @@ -620,14 +613,14 @@ public class LauncherStateTransitionAnimation { if (listener != null) { reveal.addListener(listener); } - mStateAnimation.play(reveal); + animation.play(reveal); } dispatchOnLauncherTransitionPrepare(fromView, animated, true); dispatchOnLauncherTransitionPrepare(toView, animated, true); } - mStateAnimation.addListener(new AnimatorListenerAdapter() { + animation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { fromView.setVisibility(View.GONE); @@ -657,35 +650,37 @@ public class LauncherStateTransitionAnimation { } // This can hold unnecessary references to views. - mStateAnimation = null; + cleanupAnimation(); pCb.onTransitionComplete(); } }); - final AnimatorSet stateAnimation = mStateAnimation; + final AnimatorSet stateAnimation = animation; final Runnable startAnimRunnable = new Runnable() { public void run() { - // Check that mStateAnimation hasn't changed while + // Check that mCurrentAnimation hasn't changed while // we waited for a layout/draw pass - if (mStateAnimation != stateAnimation) + if (mCurrentAnimation != stateAnimation) return; + dispatchOnLauncherTransitionStart(fromView, animated, false); dispatchOnLauncherTransitionStart(toView, animated, false); // Enable all necessary layers - boolean isLmpOrAbove = Utilities.isLmpOrAbove(); for (View v : layerViews.keySet()) { if (layerViews.get(v) == BUILD_AND_SET_LAYER) { v.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) { + if (Utilities.ATLEAST_LOLLIPOP && Utilities.isViewAttachedToWindow(v)) { v.buildLayer(); } } - mStateAnimation.start(); + stateAnimation.start(); } }; fromView.post(startAnimRunnable); + + return animation; } else { fromView.setVisibility(View.GONE); dispatchOnLauncherTransitionPrepare(fromView, animated, true); @@ -700,9 +695,44 @@ public class LauncherStateTransitionAnimation { if (onCompleteRunnable != null) { onCompleteRunnable.run(); } + + return null; } } + /** + * Coordinates the workspace search bar animation along with the launcher state animation. + */ + private void startWorkspaceSearchBarAnimation(AnimatorSet animation, + final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, int duration, + View overlaySearchBar) { + final SearchDropTargetBar.State toSearchBarState = + toWorkspaceState.getSearchDropTargetBarState(); + + if (overlaySearchBar != null) { + if ((toWorkspaceState == Workspace.State.NORMAL) && + (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN)) { + // If we are transitioning from the overlay to the workspace, then show the + // workspace search bar immediately and let the overlay search bar fade out on top + mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0); + } else if (fromWorkspaceState == Workspace.State.NORMAL) { + // If we are transitioning from the workspace to the overlay, then keep the + // workspace search bar visible until the overlay search bar fades in on top + animation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0); + } + }); + } else { + // Otherwise, then just animate the workspace search bar normally + mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration); + } + } else { + // If there is no overlay search bar, then just animate the workspace search bar + mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration); + } + } /** * Dispatches the prepare-transition event to suitable views. @@ -753,10 +783,14 @@ public class LauncherStateTransitionAnimation { * Cancels the current animation. */ private void cancelAnimation() { - if (mStateAnimation != null) { - mStateAnimation.setDuration(0); - mStateAnimation.cancel(); - mStateAnimation = null; + if (mCurrentAnimation != null) { + mCurrentAnimation.setDuration(0); + mCurrentAnimation.cancel(); + mCurrentAnimation = null; } } + + @Thunk void cleanupAnimation() { + mCurrentAnimation = null; + } } diff --git a/src/com/android/launcher3/LauncherViewPropertyAnimator.java b/src/com/android/launcher3/LauncherViewPropertyAnimator.java index 4cafbbfa6..4406a2c5c 100644 --- a/src/com/android/launcher3/LauncherViewPropertyAnimator.java +++ b/src/com/android/launcher3/LauncherViewPropertyAnimator.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.EnumSet; public class LauncherViewPropertyAnimator extends Animator implements AnimatorListener { + enum Properties { TRANSLATION_X, TRANSLATION_Y, @@ -51,13 +52,12 @@ public class LauncherViewPropertyAnimator extends Animator implements AnimatorLi long mStartDelay; long mDuration; TimeInterpolator mInterpolator; - ArrayList<Animator.AnimatorListener> mListeners; + ArrayList<Animator.AnimatorListener> mListeners = new ArrayList<>(); boolean mRunning = false; FirstFrameAnimatorHelper mFirstFrameHelper; public LauncherViewPropertyAnimator(View target) { mTarget = target; - mListeners = new ArrayList<Animator.AnimatorListener>(); } @Override diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 218c1a36f..05f0a0553 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -2293,7 +2293,7 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc // Besides disabling the accessibility long-click, this also prevents this view from getting // accessibility focus. info.setLongClickable(false); - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); } } diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java index 08f8e5601..40eadabd5 100644 --- a/src/com/android/launcher3/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/PendingAppWidgetHostView.java @@ -16,13 +16,17 @@ package com.android.launcher3; +import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.text.Layout; import android.text.StaticLayout; @@ -32,6 +36,8 @@ import android.view.View; import android.view.View.OnClickListener; public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implements OnClickListener { + private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5; + private static final float MIN_SATUNATION = 0.7f; private static Theme sPreloaderTheme; @@ -47,13 +53,14 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen private Bitmap mIcon; private Drawable mCenterDrawable; - private Drawable mTopCornerDrawable; + private Drawable mSettingIconDrawable; private boolean mDrawableSizeChanged; private final TextPaint mPaint; private Layout mSetupTextLayout; + @TargetApi(Build.VERSION_CODES.LOLLIPOP) public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info, boolean disabledForSafeMode) { super(context); @@ -70,6 +77,10 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics())); setBackgroundResource(R.drawable.quantum_panel_dark); setWillNotDraw(false); + + if (Utilities.ATLEAST_LOLLIPOP) { + setElevation(getResources().getDimension(R.dimen.pending_widget_elevation)); + } } @Override @@ -124,10 +135,12 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon); disabledIcon.setGhostModeEnabled(true); mCenterDrawable = disabledIcon; - mTopCornerDrawable = null; + mSettingIconDrawable = null; } else if (isReadyForClickSetup()) { - mCenterDrawable = getResources().getDrawable(R.drawable.ic_setting); - mTopCornerDrawable = new FastBitmapDrawable(mIcon); + mCenterDrawable = new FastBitmapDrawable(mIcon); + mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate(); + + updateSettingColor(); } else { if (sPreloaderTheme == null) { sPreloaderTheme = getResources().newTheme(); @@ -137,13 +150,25 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen FastBitmapDrawable drawable = mLauncher.createIconDrawable(mIcon); mCenterDrawable = new PreloadIconDrawable(drawable, sPreloaderTheme); mCenterDrawable.setCallback(this); - mTopCornerDrawable = null; + mSettingIconDrawable = null; applyState(); } mDrawableSizeChanged = true; } } + private void updateSettingColor() { + int color = Utilities.findDominantColorByHue(mIcon, 20); + // Make the dominant color bright. + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + hsv[1] = Math.min(hsv[1], MIN_SATUNATION); + hsv[2] = 1; + color = Color.HSVToColor(hsv); + + mSettingIconDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + @Override protected boolean verifyDrawable(Drawable who) { return (who == mCenterDrawable) || super.verifyDrawable(who); @@ -169,6 +194,83 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0; } + private void updateDrawableBounds() { + DeviceProfile grid = mLauncher.getDeviceProfile(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + + int minPadding = getResources() + .getDimensionPixelSize(R.dimen.pending_widget_min_padding); + + int availableWidth = getWidth() - paddingLeft - paddingRight - 2 * minPadding; + int availableHeight = getHeight() - paddingTop - paddingBottom - 2 * minPadding; + + if (mSettingIconDrawable == null) { + int outset = (mCenterDrawable instanceof PreloadIconDrawable) ? + ((PreloadIconDrawable) mCenterDrawable).getOutset() : 0; + int maxSize = grid.iconSizePx + 2 * outset; + int size = Math.min(maxSize, Math.min(availableWidth, availableHeight)); + + mRect.set(0, 0, size, size); + mRect.inset(outset, outset); + mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); + mCenterDrawable.setBounds(mRect); + } else { + float iconSize = Math.min(availableWidth, availableHeight); + + // Use twice the setting size factor, as the setting is drawn at a corner and the + // icon is drawn in the center. + float settingIconScaleFactor = 1 + SETUP_ICON_SIZE_FACTOR * 2; + int maxSize = Math.max(availableWidth, availableHeight); + if (iconSize * settingIconScaleFactor > maxSize) { + // There is an overlap + iconSize = maxSize / settingIconScaleFactor; + } + + int actualIconSize = (int) Math.min(iconSize, grid.iconSizePx); + + // Recreate the setup text. + mSetupTextLayout = new StaticLayout( + getResources().getText(R.string.gadget_setup_text), mPaint, availableWidth, + Layout.Alignment.ALIGN_CENTER, 1, 0, true); + int textHeight = mSetupTextLayout.getHeight(); + + // Extra icon size due to the setting icon + float minHeightWithText = textHeight + actualIconSize * settingIconScaleFactor + + grid.iconDrawablePaddingPx; + + int iconTop; + if (minHeightWithText < availableHeight) { + // We can draw the text as well + iconTop = (getHeight() - textHeight - + grid.iconDrawablePaddingPx - actualIconSize) / 2; + + } else { + // The text will not fit. Only draw the icons. + iconTop = (getHeight() - actualIconSize) / 2; + mSetupTextLayout = null; + } + + mRect.set(0, 0, actualIconSize, actualIconSize); + mRect.offset((getWidth() - actualIconSize) / 2, iconTop); + mCenterDrawable.setBounds(mRect); + + mRect.left = paddingLeft + minPadding; + mRect.right = mRect.left + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); + mRect.top = paddingTop + minPadding; + mRect.bottom = mRect.top + (int) (SETUP_ICON_SIZE_FACTOR * actualIconSize); + mSettingIconDrawable.setBounds(mRect); + + if (mSetupTextLayout != null) { + // Set up position for dragging the text + mRect.left = paddingLeft + minPadding; + mRect.top = mCenterDrawable.getBounds().bottom + grid.iconDrawablePaddingPx; + } + } + } + @Override protected void onDraw(Canvas canvas) { if (mCenterDrawable == null) { @@ -176,81 +278,21 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView implemen return; } - DeviceProfile grid = mLauncher.getDeviceProfile(); - if (mTopCornerDrawable == null) { - if (mDrawableSizeChanged) { - int outset = (mCenterDrawable instanceof PreloadIconDrawable) ? - ((PreloadIconDrawable) mCenterDrawable).getOutset() : 0; - int maxSize = grid.iconSizePx + 2 * outset; - int size = Math.min(maxSize, Math.min( - getWidth() - getPaddingLeft() - getPaddingRight(), - getHeight() - getPaddingTop() - getPaddingBottom())); - - mRect.set(0, 0, size, size); - mRect.inset(outset, outset); - mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); - mCenterDrawable.setBounds(mRect); - mDrawableSizeChanged = false; - } - mCenterDrawable.draw(canvas); - } else { - // Draw the top corner icon and "Setup" text is possible - if (mDrawableSizeChanged) { - int iconSize = grid.iconSizePx; - int paddingTop = getPaddingTop(); - int paddingBottom = getPaddingBottom(); - int paddingLeft = getPaddingLeft(); - int paddingRight = getPaddingRight(); - - int availableWidth = getWidth() - paddingLeft - paddingRight; - int availableHeight = getHeight() - paddingTop - paddingBottom; - - // Recreate the setup text. - mSetupTextLayout = new StaticLayout( - getResources().getText(R.string.gadget_setup_text), mPaint, availableWidth, - Layout.Alignment.ALIGN_CENTER, 1, 0, true); - if (mSetupTextLayout.getLineCount() == 1) { - // The text fits in a single line. No need to draw the setup icon. - int size = Math.min(iconSize, Math.min(availableWidth, - availableHeight - mSetupTextLayout.getHeight())); - mRect.set(0, 0, size, size); - mRect.offsetTo((getWidth() - mRect.width()) / 2, - (getHeight() - mRect.height() - mSetupTextLayout.getHeight() - - grid.iconDrawablePaddingPx) / 2); - - mTopCornerDrawable.setBounds(mRect); - - // Update left and top to indicate the position where the text will be drawn. - mRect.left = paddingLeft; - mRect.top = mRect.bottom + grid.iconDrawablePaddingPx; - } else { - // The text can't be drawn in a single line. Draw a setup icon instead. - mSetupTextLayout = null; - int size = Math.min(iconSize, Math.min( - getWidth() - paddingLeft - paddingRight, - getHeight() - paddingTop - paddingBottom)); - mRect.set(0, 0, size, size); - mRect.offsetTo((getWidth() - mRect.width()) / 2, (getHeight() - mRect.height()) / 2); - mCenterDrawable.setBounds(mRect); - - size = Math.min(size / 2, - Math.max(mRect.top - paddingTop, mRect.left - paddingLeft)); - mTopCornerDrawable.setBounds(paddingLeft, paddingTop, - paddingLeft + size, paddingTop + size); - } - mDrawableSizeChanged = false; - } + if (mDrawableSizeChanged) { + updateDrawableBounds(); + mDrawableSizeChanged = false; + } - if (mSetupTextLayout == null) { - mCenterDrawable.draw(canvas); - mTopCornerDrawable.draw(canvas); - } else { - canvas.save(); - canvas.translate(mRect.left, mRect.top); - mSetupTextLayout.draw(canvas); - canvas.restore(); - mTopCornerDrawable.draw(canvas); - } + mCenterDrawable.draw(canvas); + if (mSettingIconDrawable != null) { + mSettingIconDrawable.draw(canvas); } + if (mSetupTextLayout != null) { + canvas.save(); + canvas.translate(mRect.left, mRect.top); + mSetupTextLayout.draw(canvas); + canvas.restore(); + } + } } diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java index bcb59c448..45e4b2c4a 100644 --- a/src/com/android/launcher3/PreloadIconDrawable.java +++ b/src/com/android/launcher3/PreloadIconDrawable.java @@ -213,6 +213,10 @@ class PreloadIconDrawable extends Drawable { return mAnimationProgress; } + public boolean hasNotCompleted() { + return mAnimationProgress < ANIMATION_PROGRESS_COMPLETED; + } + @Override public int getIntrinsicHeight() { return mIcon.getIntrinsicHeight(); diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java index 4cdf1cac9..772a334b9 100644 --- a/src/com/android/launcher3/SearchDropTargetBar.java +++ b/src/com/android/launcher3/SearchDropTargetBar.java @@ -18,32 +18,57 @@ package com.android.launcher3; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AccelerateInterpolator; import android.widget.FrameLayout; +import com.android.launcher3.util.Thunk; + /* * Ths bar will manage the transition between the QSB search bar and the delete drop * targets so that each of the individual IconDropTargets don't have to. */ public class SearchDropTargetBar extends FrameLayout implements DragController.DragListener { - private static final int TRANSITION_DURATION = 200; + /** The different states that the search bar space can be in. */ + public enum State { + INVISIBLE (0f, 0f), + SEARCH_BAR (1f, 0f), + DROP_TARGET (0f, 1f); + + private final float mSearchBarAlpha; + private final float mDropTargetBarAlpha; + + State(float sbAlpha, float dtbAlpha) { + mSearchBarAlpha = sbAlpha; + mDropTargetBarAlpha = dtbAlpha; + } + + float getSearchBarAlpha() { + return mSearchBarAlpha; + } + + float getDropTargetBarAlpha() { + return mDropTargetBarAlpha; + } + } - private ObjectAnimator mShowDropTargetBarAnim; - private ValueAnimator mHideSearchBarAnim; + private static int DEFAULT_DRAG_FADE_DURATION = 175; + + private LauncherViewPropertyAnimator mDropTargetBarAnimator; + private LauncherViewPropertyAnimator mQSBSearchBarAnimator; private static final AccelerateInterpolator sAccelerateInterpolator = new AccelerateInterpolator(); - private boolean mIsSearchBarHidden; - private View mQSBSearchBar; - private View mDropTargetBar; + private State mState = State.SEARCH_BAR; + @Thunk View mQSB; + @Thunk View mDropTargetBar; private boolean mDeferOnDragEnd = false; + @Thunk boolean mAccessibilityEnabled = false; // Drop targets private ButtonDropTarget mInfoDropTarget; @@ -75,39 +100,6 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D mUninstallDropTarget.setLauncher(launcher); } - public void setQsbSearchBar(View qsb) { - mQSBSearchBar = qsb; - if (mQSBSearchBar != null) { - mHideSearchBarAnim = LauncherAnimUtils.ofFloat(mQSBSearchBar, "alpha", 1f, 0f); - setupAnimation(mHideSearchBarAnim, mQSBSearchBar); - } else { - // Create a no-op animation of the search bar is null - mHideSearchBarAnim = ValueAnimator.ofFloat(0, 0); - mHideSearchBarAnim.setDuration(TRANSITION_DURATION); - } - } - - private void prepareStartAnimation(View v) { - // Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd - // callback below) - if (v != null) { - v.setLayerType(View.LAYER_TYPE_HARDWARE, null); - } - } - - private void setupAnimation(ValueAnimator anim, final View v) { - anim.setInterpolator(sAccelerateInterpolator); - anim.setDuration(TRANSITION_DURATION); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (v != null) { - v.setLayerType(View.LAYER_TYPE_NONE, null); - } - } - }); - } - @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -124,72 +116,89 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D // Create the various fade animations mDropTargetBar.setAlpha(0f); - mShowDropTargetBarAnim = LauncherAnimUtils.ofFloat(mDropTargetBar, "alpha", 0f, 1f); - setupAnimation(mShowDropTargetBarAnim, mDropTargetBar); - } + mDropTargetBarAnimator = new LauncherViewPropertyAnimator(mDropTargetBar); + mDropTargetBarAnimator.setInterpolator(sAccelerateInterpolator); + mDropTargetBarAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Ensure that the view is visible for the animation + mDropTargetBar.setVisibility(View.VISIBLE); + } - /** - * Finishes all the animations on the search and drop target bars. - */ - public void finishAnimations() { - prepareStartAnimation(mDropTargetBar); - mShowDropTargetBarAnim.reverse(); - prepareStartAnimation(mQSBSearchBar); - mHideSearchBarAnim.reverse(); + @Override + public void onAnimationEnd(Animator animation) { + if (mDropTargetBar != null) { + AlphaUpdateListener.updateVisibility(mDropTargetBar, mAccessibilityEnabled); + } + } + }); } - /** - * Shows the search bar. - */ - public void showSearchBar(boolean animated) { - if (!mIsSearchBarHidden) return; - if (animated) { - prepareStartAnimation(mQSBSearchBar); - mHideSearchBarAnim.reverse(); + public void setQsbSearchBar(View qsb) { + mQSB = qsb; + if (mQSB != null) { + // Update the search ber animation + mQSBSearchBarAnimator = new LauncherViewPropertyAnimator(mQSB); + mQSBSearchBarAnimator.setInterpolator(sAccelerateInterpolator); + mQSBSearchBarAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Ensure that the view is visible for the animation + if (mQSB != null) { + mQSB.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mQSB != null) { + AlphaUpdateListener.updateVisibility(mQSB, mAccessibilityEnabled); + } + } + }); } else { - mHideSearchBarAnim.cancel(); - if (mQSBSearchBar != null) { - mQSBSearchBar.setAlpha(1f); - } + mQSBSearchBarAnimator = null; } - mIsSearchBarHidden = false; } /** - * Hides the search bar. We only use this for clings. + * Animates the current search bar state to a new state. If the {@param duration} is 0, then + * the state is applied immediately. */ - public void hideSearchBar(boolean animated) { - if (mIsSearchBarHidden) return; - if (animated) { - prepareStartAnimation(mQSBSearchBar); - mHideSearchBarAnim.start(); - } else { - mHideSearchBarAnim.cancel(); - if (mQSBSearchBar != null) { - mQSBSearchBar.setAlpha(0f); - } + public void animateToState(State newState, int duration) { + if (mState != newState) { + mState = newState; + + // Update the accessibility state + AccessibilityManager am = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + mAccessibilityEnabled = am.isEnabled(); + + animateViewAlpha(mQSBSearchBarAnimator, mQSB, newState.getSearchBarAlpha(), + duration); + animateViewAlpha(mDropTargetBarAnimator, mDropTargetBar, newState.getDropTargetBarAlpha(), + duration); } - mIsSearchBarHidden = true; } /** - * Shows the drop target bar. + * Convenience method to animate the alpha of a view using hardware layers. */ - public void showDeleteTarget() { - // Animate out the QSB search bar, and animate in the drop target bar - prepareStartAnimation(mDropTargetBar); - mShowDropTargetBarAnim.start(); - hideSearchBar(true); - } + private void animateViewAlpha(LauncherViewPropertyAnimator animator, View v, float alpha, + int duration) { + if (v == null) { + return; + } - /** - * Hides the drop target bar. - */ - public void hideDeleteTarget() { - // Restore the QSB search bar, and animate out the drop target bar - prepareStartAnimation(mDropTargetBar); - mShowDropTargetBarAnim.reverse(); - showSearchBar(true); + animator.cancel(); + if (Float.compare(v.getAlpha(), alpha) != 0) { + if (duration > 0) { + animator.alpha(alpha).withLayer().setDuration(duration).start(); + } else { + v.setAlpha(alpha); + AlphaUpdateListener.updateVisibility(v, mAccessibilityEnabled); + } + } } /* @@ -197,9 +206,13 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D */ @Override public void onDragStart(DragSource source, Object info, int dragAction) { - showDeleteTarget(); + animateToState(State.DROP_TARGET, DEFAULT_DRAG_FADE_DURATION); } + /** + * This is called to defer hiding the delete drop target until the drop animation has completed, + * instead of hiding immediately when the drag has ended. + */ public void deferOnDragEnd() { mDeferOnDragEnd = true; } @@ -207,22 +220,25 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D @Override public void onDragEnd() { if (!mDeferOnDragEnd) { - hideDeleteTarget(); + animateToState(State.SEARCH_BAR, DEFAULT_DRAG_FADE_DURATION); } else { mDeferOnDragEnd = false; } } + /** + * @return the bounds of the QSB search bar. + */ public Rect getSearchBarBounds() { - if (mQSBSearchBar != null) { + if (mQSB != null) { final int[] pos = new int[2]; - mQSBSearchBar.getLocationOnScreen(pos); + mQSB.getLocationOnScreen(pos); final Rect rect = new Rect(); rect.left = pos[0]; rect.top = pos[1]; - rect.right = pos[0] + mQSBSearchBar.getWidth(); - rect.bottom = pos[1] + mQSBSearchBar.getHeight(); + rect.right = pos[0] + mQSB.getWidth(); + rect.bottom = pos[1] + mQSB.getHeight(); return rect; } else { return null; @@ -230,8 +246,8 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D } public void enableAccessibleDrag(boolean enable) { - if (mQSBSearchBar != null) { - mQSBSearchBar.setVisibility(enable ? View.GONE : View.VISIBLE); + if (mQSB != null) { + mQSB.setVisibility(enable ? View.GONE : View.VISIBLE); } mInfoDropTarget.enableAccessibleDrag(enable); mDeleteDropTarget.enableAccessibleDrag(enable); diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index 56c0b9d2f..5766cf2f2 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -198,13 +198,17 @@ public class ShortcutInfo extends ItemInfo { return mIcon; } - public void updateIcon(IconCache iconCache) { + public void updateIcon(IconCache iconCache, boolean useLowRes) { if (itemType == Favorites.ITEM_TYPE_APPLICATION) { iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user, - shouldUseLowResIcon()); + useLowRes); } } + public void updateIcon(IconCache iconCache) { + updateIcon(iconCache, shouldUseLowResIcon()); + } + @Override void onAddToDatabase(Context context, ContentValues values) { super.onAddToDatabase(context, values); diff --git a/src/com/android/launcher3/StylusEventHelper.java b/src/com/android/launcher3/StylusEventHelper.java index da46e6a54..e03273a6e 100644 --- a/src/com/android/launcher3/StylusEventHelper.java +++ b/src/com/android/launcher3/StylusEventHelper.java @@ -1,8 +1,5 @@ package com.android.launcher3; -import com.android.launcher3.Utilities; - -import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; @@ -77,8 +74,9 @@ public class StylusEventHelper { * @param event The event to check. * @return Whether a stylus button press occurred. */ - public static boolean isStylusButtonPressed(MotionEvent event) { + private static boolean isStylusButtonPressed(MotionEvent event) { return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS - && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY); + && ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) + == MotionEvent.BUTTON_SECONDARY); } }
\ No newline at end of file diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java index 0819f8ce0..955d4013c 100644 --- a/src/com/android/launcher3/UninstallDropTarget.java +++ b/src/com/android/launcher3/UninstallDropTarget.java @@ -38,7 +38,7 @@ public class UninstallDropTarget extends ButtonDropTarget { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public static boolean supportsDrop(Context context, Object info) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (Utilities.ATLEAST_JB_MR2) { UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); Bundle restrictions = userManager.getUserRestrictions(); if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 8fd298df7..adedd33b2 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -87,6 +87,24 @@ public final class Utilities { private static final int[] sLoc0 = new int[2]; private static final int[] sLoc1 = new int[2]; + // TODO: use Build.VERSION_CODES when available + public static final boolean ATLEAST_MARSHMALLOW = Build.VERSION.SDK_INT >= 23; + + public static final boolean ATLEAST_LOLLIPOP_MR1 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; + + public static final boolean ATLEAST_LOLLIPOP = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + + public static final boolean ATLEAST_KITKAT = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + public static final boolean ATLEAST_JB_MR1 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean ATLEAST_JB_MR2 = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + // To turn on these properties, type // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; @@ -110,23 +128,6 @@ public final class Utilities { return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); } - /** - * Indicates if the device is running LMP or higher. - */ - public static boolean isLmpOrAbove() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; - } - - public static boolean isLmpMR1OrAbove() { - // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; - return Build.VERSION.SDK_INT >= 22; - } - - public static boolean isLmpMR1() { - // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; - return Build.VERSION.SDK_INT == 22; - } - public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { byte[] data = c.getBlob(iconIndex); try { @@ -517,7 +518,7 @@ public final class Utilities { @TargetApi(Build.VERSION_CODES.KITKAT) public static boolean isViewAttachedToWindow(View v) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (ATLEAST_KITKAT) { return v.isAttachedToWindow(); } else { // A proxy call which returns null, if the view is not attached to the window. @@ -544,7 +545,7 @@ public final class Utilities { AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) { if (info.provider.getPackageName().equals(providerPkg)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (ATLEAST_JB_MR1) { if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) { return info; } else if (defaultWidgetForSearchPackage == null) { @@ -655,7 +656,7 @@ public final class Utilities { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static boolean isRtl(Resources res) { - return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) && + return ATLEAST_JB_MR1 && (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); } diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java index 629387ed0..346055566 100644 --- a/src/com/android/launcher3/WidgetPreviewLoader.java +++ b/src/com/android/launcher3/WidgetPreviewLoader.java @@ -105,7 +105,7 @@ public class WidgetPreviewLoader { * sizes (landscape vs portrait). */ private static class CacheDb extends SQLiteOpenHelper { - private static final int DB_VERSION = 3; + private static final int DB_VERSION = 4; private static final String TABLE_NAME = "shortcut_and_widget_previews"; private static final String COLUMN_COMPONENT = "componentName"; @@ -212,7 +212,6 @@ public class WidgetPreviewLoader { public void removeObsoletePreviews(ArrayList<Object> list) { Utilities.assertWorkerThread(); - LongSparseArray<UserHandleCompat> userIdCache = new LongSparseArray<>(); LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>(); for (Object obj : list) { @@ -227,15 +226,7 @@ public class WidgetPreviewLoader { pkg = info.provider.getPackageName(); } - int userIdIndex = userIdCache.indexOfValue(user); - final long userId; - if (userIdIndex < 0) { - userId = mUserManager.getSerialNumberForUser(user); - userIdCache.put(userId, user); - } else { - userId = userIdCache.keyAt(userIdIndex); - } - + final long userId = mUserManager.getSerialNumberForUser(user); HashSet<String> packages = validPackages.get(userId); if (packages == null) { packages = new HashSet<>(); @@ -361,8 +352,8 @@ public class WidgetPreviewLoader { } final boolean widgetPreviewExists = (drawable != null); - final int spanX = info.getSpanX(launcher) < 1 ? 1 : info.getSpanX(launcher); - final int spanY = info.getSpanY(launcher) < 1 ? 1 : info.getSpanY(launcher); + final int spanX = info.spanX; + final int spanY = info.spanY; int previewWidth; int previewHeight; @@ -386,7 +377,7 @@ public class WidgetPreviewLoader { preScaledWidthOut[0] = previewWidth; } if (previewWidth > maxPreviewWidth) { - scale = maxPreviewWidth / (float) previewWidth; + scale = (maxPreviewWidth - 2 * mProfileBadgeMargin) / (float) (previewWidth); } if (scale != 1f) { previewWidth = (int) (scale * previewWidth); @@ -405,7 +396,7 @@ public class WidgetPreviewLoader { } // Draw the scaled preview into the final bitmap - int x = (preview.getWidth() - previewWidth - mProfileBadgeMargin) / 2; + int x = (preview.getWidth() - previewWidth) / 2; if (widgetPreviewExists) { drawable.setBounds(x, 0, x + previewWidth, previewHeight); drawable.draw(c); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 4a6b90afe..856e3b88a 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -178,7 +178,24 @@ public class Workspace extends PagedView // State variable that indicates whether the pages are small (ie when you're // in all apps or customize mode) - enum State { NORMAL, NORMAL_HIDDEN, SPRING_LOADED, OVERVIEW, OVERVIEW_HIDDEN}; + enum State { + NORMAL (SearchDropTargetBar.State.SEARCH_BAR), + NORMAL_HIDDEN (SearchDropTargetBar.State.INVISIBLE), + SPRING_LOADED (SearchDropTargetBar.State.DROP_TARGET), + OVERVIEW (SearchDropTargetBar.State.INVISIBLE), + OVERVIEW_HIDDEN (SearchDropTargetBar.State.INVISIBLE); + + private final SearchDropTargetBar.State mBarState; + + State(SearchDropTargetBar.State searchBarState) { + mBarState = searchBarState; + } + + public SearchDropTargetBar.State getSearchDropTargetBarState() { + return mBarState; + } + }; + private State mState = State.NORMAL; private boolean mIsSwitchingState = false; @@ -306,8 +323,9 @@ public class Workspace extends PagedView TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0); mSpringLoadedShrinkFactor = - res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; - mOverviewModeShrinkFactor = grid.getOverviewModeScale(mIsRtl); + res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; + mOverviewModeShrinkFactor = + res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100f; mOriginalDefaultPage = mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); a.recycle(); @@ -869,6 +887,9 @@ public class Workspace extends PagedView } } + LauncherAccessibilityDelegate delegate = + LauncherAppState.getInstance().getAccessibilityDelegate(); + // We enforce at least one page to add new items to. In the case that we remove the last // such screen, we convert the last screen to the empty screen int minScreens = 1 + numCustomPages(); @@ -883,6 +904,11 @@ public class Workspace extends PagedView if (indexOfChild(cl) < currentPage) { pageShift++; } + + if (delegate != null && delegate.isInAccessibleDrag()) { + cl.enableAccessibleDrag(false, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); + } + removeView(cl); } else { // if this is the last non-custom content screen, convert it to the empty screen @@ -1566,7 +1592,7 @@ public class Workspace extends PagedView // Reset our click listener setOnClickListener(mLauncher); } - mLauncher.getSearchBar().enableAccessibleDrag(enable); + mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable); mLauncher.getHotseat().getLayout() .enableAccessibleDrag(enable, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG); } @@ -1952,15 +1978,17 @@ public class Workspace extends PagedView int getOverviewModeTranslationY() { DeviceProfile grid = mLauncher.getDeviceProfile(); - Rect overviewBar = grid.getOverviewModeButtonBarRect(); + Rect workspacePadding = grid.getWorkspacePadding(Utilities.isRtl(getResources())); + int overviewButtonBarHeight = grid.getOverviewModeButtonBarHeight(); - int availableHeight = getViewportHeight(); int scaledHeight = (int) (mOverviewModeShrinkFactor * getNormalChildHeight()); - int offsetFromTopEdge = (availableHeight - scaledHeight) / 2; - int offsetToCenterInOverview = (availableHeight - mInsets.top - overviewBar.height() - - scaledHeight) / 2; - - return -offsetFromTopEdge + mInsets.top + offsetToCenterInOverview; + int workspaceTop = mInsets.top + workspacePadding.top; + int workspaceBottom = getViewportHeight() - mInsets.bottom - workspacePadding.bottom; + int overviewTop = mInsets.top; + int overviewBottom = getViewportHeight() - mInsets.bottom - overviewButtonBarHeight; + int workspaceOffsetTopEdge = workspaceTop + ((workspaceBottom - workspaceTop) - scaledHeight) / 2; + int overviewOffsetTopEdge = overviewTop + (overviewBottom - overviewTop - scaledHeight) / 2; + return -workspaceOffsetTopEdge + overviewOffsetTopEdge; } /** @@ -1968,10 +1996,10 @@ public class Workspace extends PagedView * to that new state. */ public Animator setStateWithAnimation(State toState, int toPage, boolean animated, - boolean hasOverlaySearchBar, HashMap<View, Integer> layerViews) { + HashMap<View, Integer> layerViews) { // Create the animation to the new state Animator workspaceAnim = mStateTransitionAnimation.getAnimationToState(mState, - toState, toPage, animated, hasOverlaySearchBar, layerViews); + toState, toPage, animated, layerViews); // Update the current state mState = toState; @@ -1985,7 +2013,7 @@ public class Workspace extends PagedView } public void updateAccessibilityFlags() { - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { int total = getPageCount(); for (int i = numCustomPages(); i < total; i++) { updateAccessibilityFlags((CellLayout) getPageAt(i), i); @@ -2273,6 +2301,8 @@ public class Workspace extends PagedView dragRect = new Rect(left, top, right, bottom); } else if (child instanceof FolderIcon) { int previewSize = grid.folderIconSizePx; + dragVisualizeOffset = new Point(-padding.get() / 2, + padding.get() / 2 - child.getPaddingTop()); dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize); } @@ -2289,14 +2319,14 @@ public class Workspace extends PagedView throw new IllegalStateException(msg); } - DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), - DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); - dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); - if (child.getParent() instanceof ShortcutAndWidgetContainer) { mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent(); } + DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), + DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); + dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor()); + b.recycle(); } @@ -3763,7 +3793,11 @@ public class Workspace extends PagedView if (parentCell != null) { parentCell.removeView(v); } else if (LauncherAppState.isDogfoodBuild()) { - throw new NullPointerException("mDragInfo.cell has null parent"); + // When an app is uninstalled using the drop target, we wait until resume to remove + // the icon. We also remove all the corresponding items from the workspace at + // {@link Launcher#bindComponentsRemoved}. That call can come before or after + // {@link Launcher#mOnResumeCallbacks} depending on how busy the worker thread is. + Log.e(TAG, "mDragInfo.cell has null parent"); } if (v instanceof DropTarget) { mDragController.removeDropTarget((DropTarget) v); @@ -4050,6 +4084,16 @@ public class Workspace extends PagedView }); } + public View getHomescreenIconByItemId(final long id) { + return getFirstMatch(new ItemOperator() { + + @Override + public boolean evaluate(ItemInfo info, View v, View parent) { + return info != null && info.id == id; + } + }); + } + public View getViewForTag(final Object tag) { return getFirstMatch(new ItemOperator() { @@ -4298,8 +4342,9 @@ public class Workspace extends PagedView updates.contains(info)) { ShortcutInfo si = (ShortcutInfo) info; BubbleTextView shortcut = (BubbleTextView) v; - boolean oldPromiseState = getTextViewIcon(shortcut) - instanceof PreloadIconDrawable; + Drawable oldIcon = getTextViewIcon(shortcut); + boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable) + && ((PreloadIconDrawable) oldIcon).hasNotCompleted(); shortcut.applyFromShortcutInfo(si, mIconCache, si.isPromise() != oldPromiseState); diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java index b8916a72b..54f63bbd8 100644 --- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -211,22 +211,20 @@ public class WorkspaceStateTransitionAnimation { mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime); mSpringLoadedShrinkFactor = res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f; + mOverviewModeShrinkFactor = + res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100f; mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f; - mOverviewModeShrinkFactor = grid.getOverviewModeScale(Utilities.isRtl(res)); mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens(); } public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState, - int toPage, boolean animated, boolean hasOverlaySearchBar, - HashMap<View, Integer> layerViews) { + int toPage, boolean animated, HashMap<View, Integer> layerViews) { AccessibilityManager am = (AccessibilityManager) mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE); final boolean accessibilityEnabled = am.isEnabled(); TransitionStates states = new TransitionStates(fromState, toState); - int duration = getAnimationDuration(states); - animateWorkspace(states, toPage, animated, duration, layerViews, - accessibilityEnabled); - animateSearchBar(states, animated, duration, hasOverlaySearchBar, layerViews, + int workspaceDuration = getAnimationDuration(states); + animateWorkspace(states, toPage, animated, workspaceDuration, layerViews, accessibilityEnabled); animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION); return mStateAnimator; @@ -473,75 +471,9 @@ public class WorkspaceStateTransitionAnimation { } /** - * Coordinates with the workspace animation to animate the search bar. - * - * TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the - * bar has no idea that it is hidden, and this has no idea what state the bar is - * actually in. - */ - private void animateSearchBar(TransitionStates states, boolean animated, int duration, - boolean hasOverlaySearchBar, final HashMap<View, Integer> layerViews, - final boolean accessibilityEnabled) { - - // The search bar is only visible in the workspace - final View searchBar = mLauncher.getOrCreateQsbBar(); - if (searchBar != null) { - final boolean searchBarWillBeShown = states.stateIsNormal; - final float finalSearchBarAlpha = searchBarWillBeShown ? 1f : 0f; - if (animated) { - if (hasOverlaySearchBar) { - // If there is an overlay search bar, then we will coordinate with it. - mStateAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - // If we are transitioning to a visible search bar, show it immediately - // and let the overlay search bar has faded out - if (searchBarWillBeShown) { - searchBar.setAlpha(finalSearchBarAlpha); - AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - // If we are transitioning to a hidden search bar, hide it only after - // the overlay search bar has faded in - if (!searchBarWillBeShown) { - searchBar.setAlpha(finalSearchBarAlpha); - AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); - } - } - }); - } else { - // Otherwise, we can just do the normal animation - LauncherViewPropertyAnimator searchBarAlpha = - new LauncherViewPropertyAnimator(searchBar).alpha(finalSearchBarAlpha); - searchBarAlpha.addListener(new AlphaUpdateListener(searchBar, - accessibilityEnabled)); - searchBar.setLayerType(View.LAYER_TYPE_HARDWARE, null); - if (layerViews != null) { - // If layerViews is not null, we add these views, and indicate that - // the caller can manage layer state. - layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER); - } else { - // Otherwise let the animator handle layer management. - searchBarAlpha.withLayer(); - } - searchBarAlpha.setDuration(duration); - mStateAnimator.play(searchBarAlpha); - } - } else { - // Set the search bar state immediately - searchBar.setAlpha(finalSearchBarAlpha); - AlphaUpdateListener.updateVisibility(searchBar, accessibilityEnabled); - } - } - } - - /** * Animates the background scrim. Add to the state animator to prevent jankiness. * - * @param finalAlpha the final alpha for the background scrim + * @param states the current and final workspace states * @param animated whether or not to set the background alpha immediately * @duration duration of the animation */ diff --git a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java new file mode 100644 index 000000000..117aca921 --- /dev/null +++ b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java @@ -0,0 +1,194 @@ +/* + * 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.allapps; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import android.view.Gravity; +import com.android.launcher3.R; + +/** + * A helper class to positon and orient a drawable to be drawn. + */ +class TransformedImageDrawable { + private Drawable mImage; + private float mXPercent; + private float mYPercent; + private int mGravity; + + /** + * @param gravity If one of the Gravity center values, the x and y offset will take the width + * and height of the image into account to center the image to the offset. + */ + public TransformedImageDrawable(Resources res, int resourceId, float xPct, float yPct, + int gravity) { + mImage = res.getDrawable(resourceId); + mXPercent = xPct; + mYPercent = yPct; + mGravity = gravity; + } + + public void setAlpha(int alpha) { + mImage.setAlpha(alpha); + } + + public int getAlpha() { + return mImage.getAlpha(); + } + + public void updateBounds(Rect bounds) { + int width = mImage.getIntrinsicWidth(); + int height = mImage.getIntrinsicHeight(); + int left = bounds.left + (int) (mXPercent * bounds.width()); + int top = bounds.top + (int) (mYPercent * bounds.height()); + if ((mGravity & Gravity.CENTER_HORIZONTAL) == Gravity.CENTER_HORIZONTAL) { + left -= (width / 2); + } + if ((mGravity & Gravity.CENTER_VERTICAL) == Gravity.CENTER_VERTICAL) { + top -= (height / 2); + } + mImage.setBounds(left, top, left + width, top + height); + } + + public void draw(Canvas canvas) { + int c = canvas.save(Canvas.MATRIX_SAVE_FLAG); + mImage.draw(canvas); + canvas.restoreToCount(c); + } +} + +/** + * This is a custom composite drawable that has a fixed virtual size and dynamically lays out its + * children images relatively within its bounds. This way, we can reduce the memory usage of a + * single, large sparsely populated image. + */ +public class AllAppsBackgroundDrawable extends Drawable { + + private final TransformedImageDrawable mHand; + private final TransformedImageDrawable[] mIcons; + private final int mWidth; + private final int mHeight; + + private ObjectAnimator mBackgroundAnim; + + public AllAppsBackgroundDrawable(Context context) { + Resources res = context.getResources(); + mHand = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_hand, + 0.575f, 0.1f, Gravity.CENTER_HORIZONTAL); + mIcons = new TransformedImageDrawable[4]; + mIcons[0] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_1, + 0.375f, 0, Gravity.CENTER_HORIZONTAL); + mIcons[1] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_2, + 0.3125f, 0.25f, Gravity.CENTER_HORIZONTAL); + mIcons[2] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_3, + 0.475f, 0.4f, Gravity.CENTER_HORIZONTAL); + mIcons[3] = new TransformedImageDrawable(res, R.drawable.ic_all_apps_bg_icon_4, + 0.7f, 0.125f, Gravity.CENTER_HORIZONTAL); + mWidth = res.getDimensionPixelSize(R.dimen.all_apps_background_canvas_width); + mHeight = res.getDimensionPixelSize(R.dimen.all_apps_background_canvas_height); + } + + /** + * Animates the background alpha. + */ + public void animateBgAlpha(float finalAlpha, int duration) { + int finalAlphaI = (int) (finalAlpha * 255f); + if (getAlpha() != finalAlphaI) { + mBackgroundAnim = cancelAnimator(mBackgroundAnim); + mBackgroundAnim = ObjectAnimator.ofInt(this, "alpha", finalAlphaI); + mBackgroundAnim.setDuration(duration); + mBackgroundAnim.start(); + } + } + + /** + * Sets the background alpha immediately. + */ + public void setBgAlpha(float finalAlpha) { + int finalAlphaI = (int) (finalAlpha * 255f); + if (getAlpha() != finalAlphaI) { + mBackgroundAnim = cancelAnimator(mBackgroundAnim); + setAlpha(finalAlphaI); + } + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + + @Override + public void draw(Canvas canvas) { + mHand.draw(canvas); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].draw(canvas); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mHand.updateBounds(bounds); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].updateBounds(bounds); + } + invalidateSelf(); + } + + @Override + public void setAlpha(int alpha) { + mHand.setAlpha(alpha); + for (int i = 0; i < mIcons.length; i++) { + mIcons[i].setAlpha(alpha); + } + invalidateSelf(); + } + + @Override + public int getAlpha() { + return mHand.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // Do nothing + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + private ObjectAnimator cancelAnimator(ObjectAnimator animator) { + if (animator != null) { + animator.removeAllListeners(); + animator.cancel(); + } + return null; + } +} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 67d572819..88c6acada 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -16,34 +16,26 @@ package com.android.launcher3.allapps; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.InsetDrawable; -import android.os.Build; -import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; import android.widget.LinearLayout; - import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; -import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; -import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; @@ -53,7 +45,6 @@ import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherTransitionable; import com.android.launcher3.R; -import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.util.ComponentKey; @@ -155,6 +146,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Thunk AllAppsSearchBarController mSearchBarController; private ViewGroup mSearchBarContainerView; private View mSearchBarView; + private SpannableStringBuilder mSearchQueryBuilder = null; private int mSectionNamesMargin; private int mNumAppsPerRow; @@ -165,7 +157,13 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // This coordinate is relative to its parent private final Point mIconLastTouchPos = new Point(); - private SpannableStringBuilder mSearchQueryBuilder = null; + private View.OnClickListener mSearchClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent searchIntent = (Intent) v.getTag(); + mLauncher.startActivitySafely(v, searchIntent, null); + } + }; public AllAppsContainerView(Context context) { this(context, null); @@ -182,8 +180,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mLauncher = (Launcher) context; mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin); mApps = new AlphabeticalAppsList(context); - mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this); - mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message)); + mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mItemDecoration = mAdapter.getItemDecoration(); @@ -528,7 +525,6 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); ItemInfo itemInfo = (ItemInfo) d.dragInfo; if (layout != null) { - layout.calculateSpans(itemInfo); showOutOfSpaceMessage = !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); } @@ -559,8 +555,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) { if (toWorkspace) { - // Reset the search bar after transitioning home + // Reset the search bar and base recycler view after transitioning home mSearchBarController.reset(); + mAppsRecyclerView.reset(); } } @@ -616,19 +613,16 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onSearchResult(String query, ArrayList<ComponentKey> apps) { if (apps != null) { - if (apps.isEmpty()) { - String formatStr = getResources().getString(R.string.all_apps_no_search_results); - mAdapter.setEmptySearchText(String.format(formatStr, query)); - } else { - mAppsRecyclerView.scrollToTop(); - } mApps.setOrderedFilter(apps); + mAdapter.setLastSearchQuery(query); + mAppsRecyclerView.onSearchResultsChanged(); } } @Override public void clearSearchResult() { mApps.setOrderedFilter(null); + mAppsRecyclerView.onSearchResultsChanged(); // Clear the search query mSearchQueryBuilder.clear(); diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 057883cab..1f95133d4 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -16,21 +16,30 @@ package com.android.launcher3.allapps; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; -import android.os.Handler; +import android.graphics.drawable.Drawable; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.net.Uri; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; import com.android.launcher3.AppInfo; import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -42,7 +51,7 @@ import java.util.List; /** * The grid view adapter of all the apps. */ -class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { +public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { public static final String TAG = "AppsGridAdapter"; private static final boolean DEBUG = false; @@ -55,6 +64,10 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol public static final int PREDICTION_ICON_VIEW_TYPE = 2; // The message shown when there are no filtered results public static final int EMPTY_SEARCH_VIEW_TYPE = 3; + // A divider that separates the apps list and the search market button + public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4; + // The message to continue to a market search when there are no filtered results + public static final int SEARCH_MARKET_VIEW_TYPE = 5; /** * ViewHolder for each icon. @@ -69,6 +82,38 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } /** + * A subclass of GridLayoutManager that overrides accessibility values during app search. + */ + public class AppsGridLayoutManager extends GridLayoutManager { + + public AppsGridLayoutManager(Context context) { + super(context, 1, GridLayoutManager.VERTICAL, false); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + // Ensure that we only report the number apps for accessibility not including other + // adapter views + final AccessibilityRecordCompat record = AccessibilityEventCompat + .asRecord(event); + record.setItemCount(mApps.getNumFilteredApps()); + } + + @Override + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mApps.hasNoFilteredResults()) { + // Disregard the no-search-results text as a list item for accessibility + return 0; + } else { + return super.getRowCountForAccessibility(recycler, state); + } + } + } + + /** * Helper class to size the grid items. */ public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { @@ -80,11 +125,6 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Override public int getSpanSize(int position) { - if (mApps.hasNoFilteredResults()) { - // Empty view spans full width - return mAppsPerRow; - } - switch (mApps.getAdapterItems().get(position).viewType) { case AllAppsGridAdapter.ICON_VIEW_TYPE: case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE: @@ -279,6 +319,7 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } } + private Launcher mLauncher; private LayoutInflater mLayoutInflater; @Thunk AlphabeticalAppsList mApps; private GridLayoutManager mGridLayoutMgr; @@ -291,7 +332,19 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Thunk int mPredictionBarDividerOffset; @Thunk int mAppsPerRow; @Thunk boolean mIsRtl; - private String mEmptySearchText; + + // The text to show when there are no search results and no market search handler. + private String mEmptySearchMessage; + // The name of the market app which handles searches, to be used in the format str + // below when updating the search-market view. Only needs to be loaded once. + private String mMarketAppName; + // The text to show when there is a market app which can handle a specific query, updated + // each time the search query changes. + private String mMarketSearchMessage; + // The intent to send off to the market app, updated each time the search query changes. + private Intent mMarketSearchIntent; + // The last query that the user entered into the search field + private String mLastSearchQuery; // Section drawing @Thunk int mSectionNamesMargin; @@ -299,16 +352,18 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Thunk Paint mSectionTextPaint; @Thunk Paint mPredictedAppsDividerPaint; - public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps, + public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnTouchListener touchListener, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) { - Resources res = context.getResources(); + Resources res = launcher.getResources(); + mLauncher = launcher; mApps = apps; + mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); mGridSizer = new GridSpanSizer(); - mGridLayoutMgr = new GridLayoutManager(context, 1, GridLayoutManager.VERTICAL, false); + mGridLayoutMgr = new AppsGridLayoutManager(launcher); mGridLayoutMgr.setSpanSizeLookup(mGridSizer); mItemDecoration = new GridItemDecoration(); - mLayoutInflater = LayoutInflater.from(context); + mLayoutInflater = LayoutInflater.from(launcher); mTouchListener = touchListener; mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; @@ -328,6 +383,14 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol mPredictionBarDividerOffset = (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) + res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2; + + // Resolve the market app handling additional searches + PackageManager pm = launcher.getPackageManager(); + ResolveInfo marketInfo = pm.resolveActivity(createMarketSearchIntent(""), + PackageManager.MATCH_DEFAULT_ONLY); + if (marketInfo != null) { + mMarketAppName = marketInfo.loadLabel(pm).toString(); + } } /** @@ -346,10 +409,19 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol } /** - * Sets the text to show when there are no apps. + * Sets the last search query that was made, used to show when there are no results and to also + * seed the intent for searching the market. */ - public void setEmptySearchText(String query) { - mEmptySearchText = query; + public void setLastSearchQuery(String query) { + Resources res = mLauncher.getResources(); + String formatStr = res.getString(R.string.all_apps_no_search_results); + mLastSearchQuery = query; + mEmptySearchMessage = String.format(formatStr, query); + if (mMarketAppName != null) { + mMarketSearchMessage = String.format(res.getString(R.string.all_apps_search_market_message), + mMarketAppName); + mMarketSearchIntent = createMarketSearchIntent(query); + } } /** @@ -378,9 +450,6 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { - case EMPTY_SEARCH_VIEW_TYPE: - return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, - false)); case SECTION_BREAK_VIEW_TYPE: return new ViewHolder(new View(parent.getContext())); case ICON_VIEW_TYPE: { @@ -405,6 +474,22 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol icon.setFocusable(true); return new ViewHolder(icon); } + case EMPTY_SEARCH_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, + parent, false)); + case SEARCH_MARKET_DIVIDER_VIEW_TYPE: + return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_search_market_divider, + parent, false)); + case SEARCH_MARKET_VIEW_TYPE: + View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, + parent, false); + searchMarketView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mLauncher.startSearchFromAllApps(v, mMarketSearchIntent, mLastSearchQuery); + } + }); + return new ViewHolder(searchMarketView); default: throw new RuntimeException("Unexpected view type"); } @@ -426,28 +511,47 @@ class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHol break; } case EMPTY_SEARCH_VIEW_TYPE: - TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text); - emptyViewText.setText(mEmptySearchText); + TextView emptyViewText = (TextView) holder.mContent; + emptyViewText.setText(mEmptySearchMessage); + emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : + Gravity.START | Gravity.CENTER_VERTICAL); + break; + case SEARCH_MARKET_VIEW_TYPE: + TextView searchView = (TextView) holder.mContent; + if (mMarketSearchIntent != null) { + searchView.setVisibility(View.VISIBLE); + searchView.setContentDescription(mMarketSearchMessage); + searchView.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : + Gravity.START | Gravity.CENTER_VERTICAL); + searchView.setText(mMarketSearchMessage); + } else { + searchView.setVisibility(View.GONE); + } break; } } @Override public int getItemCount() { - if (mApps.hasNoFilteredResults()) { - // For the empty view - return 1; - } return mApps.getAdapterItems().size(); } @Override public int getItemViewType(int position) { - if (mApps.hasNoFilteredResults()) { - return EMPTY_SEARCH_VIEW_TYPE; - } - AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); return item.viewType; } + + /** + * Creates a new market search intent. + */ + private Intent createMarketSearchIntent(String query) { + Uri marketSearchUri = Uri.parse("market://search") + .buildUpon() + .appendQueryParameter("q", query) + .build(); + Intent marketSearchIntent = new Intent(Intent.ACTION_VIEW); + marketSearchIntent.setData(marketSearchUri); + return marketSearchIntent; + } } diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 730c8d15a..2f66e2cad 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -15,8 +15,11 @@ */ package com.android.launcher3.allapps; +import android.animation.ObjectAnimator; import android.content.Context; +import android.content.res.Resources; import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -26,6 +29,7 @@ import android.view.View; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.BaseRecyclerViewFastScrollBar; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; import com.android.launcher3.Stats; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -57,6 +61,9 @@ public class AllAppsRecyclerView extends BaseRecyclerView private ScrollPositionState mScrollPosState = new ScrollPositionState(); + private AllAppsBackgroundDrawable mEmptySearchBackground; + private int mEmptySearchBackgroundTopOffset; + public AllAppsRecyclerView(Context context) { this(context, null); } @@ -72,6 +79,11 @@ public class AllAppsRecyclerView extends BaseRecyclerView public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr); + + Resources res = getResources(); + mScrollbar.setDetachThumbOnFastScroll(); + mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize( + R.dimen.all_apps_empty_search_bg_top_offset); } /** @@ -90,6 +102,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView RecyclerView.RecycledViewPool pool = getRecycledViewPool(); int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx); pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1); + pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1); pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow); pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows); @@ -99,6 +113,10 @@ public class AllAppsRecyclerView extends BaseRecyclerView * Scrolls this recycler view to the top. */ public void scrollToTop() { + // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling + if (mScrollbar.isThumbDetached()) { + mScrollbar.reattachThumbToScroll(); + } scrollToPosition(0); } @@ -115,6 +133,30 @@ public class AllAppsRecyclerView extends BaseRecyclerView } @Override + public void onDraw(Canvas c) { + // Draw the background + if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) { + c.clipRect(mBackgroundPadding.left, mBackgroundPadding.top, + getWidth() - mBackgroundPadding.right, + getHeight() - mBackgroundPadding.bottom); + + mEmptySearchBackground.draw(c); + } + + super.onDraw(c); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mEmptySearchBackground || super.verifyDrawable(who); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + updateEmptySearchBackgroundBounds(); + } + + @Override protected void onFinishInflate() { super.onFinishInflate(); @@ -134,6 +176,25 @@ public class AllAppsRecyclerView extends BaseRecyclerView } } + public void onSearchResultsChanged() { + // Always scroll the view to the top so the user can see the changed results + scrollToTop(); + + if (mApps.hasNoFilteredResults()) { + if (mEmptySearchBackground == null) { + mEmptySearchBackground = new AllAppsBackgroundDrawable(getContext()); + mEmptySearchBackground.setAlpha(0); + mEmptySearchBackground.setCallback(this); + updateEmptySearchBackgroundBounds(); + } + mEmptySearchBackground.animateBgAlpha(1f, 150); + } else if (mEmptySearchBackground != null) { + // For the time being, we just immediately hide the background to ensure that it does + // not overlap with the results + mEmptySearchBackground.setBgAlpha(0f); + } + } + /** * Maps the touch (from 0..1) to the adapter position that should be visible. */ @@ -166,8 +227,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView } // Map the touch position back to the scroll of the recycler view - getCurScrollState(mScrollPosState, mApps.getAdapterItems()); - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + getCurScrollState(mScrollPosState); + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager(); if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) { layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); @@ -214,24 +275,83 @@ public class AllAppsRecyclerView extends BaseRecyclerView * Updates the bounds for the scrollbar. */ @Override - public void onUpdateScrollbar() { + public void onUpdateScrollbar(int dy) { List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); // Skip early if there are no items or we haven't been measured if (items.isEmpty() || mNumAppsPerRow == 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } // Find the index and height of the first visible row (all rows have the same height) int rowCount = mApps.getNumAppRows(); - getCurScrollState(mScrollPosState, items); + getCurScrollState(mScrollPosState); if (mScrollPosState.rowIndex < 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } - synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + // Only show the scrollbar if there is height to be scrolled + int availableScrollBarHeight = getAvailableScrollBarHeight(); + int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight); + if (availableScrollHeight <= 0) { + mScrollbar.setThumbOffset(-1, -1); + return; + } + + // Calculate the current scroll position, the scrollY of the recycler view accounts for the + // view padding, while the scrollBarY is drawn right up to the background padding (ignoring + // padding) + int scrollY = getPaddingTop() + + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset; + int scrollBarY = mBackgroundPadding.top + + (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); + + if (mScrollbar.isThumbDetached()) { + int scrollBarX; + if (Utilities.isRtl(getResources())) { + scrollBarX = mBackgroundPadding.left; + } else { + scrollBarX = getWidth() - mBackgroundPadding.right - mScrollbar.getThumbWidth(); + } + + if (mScrollbar.isDraggingThumb()) { + // If the thumb is detached, then just update the thumb to the current + // touch position + mScrollbar.setThumbOffset(scrollBarX, (int) mScrollbar.getLastTouchY()); + } else { + int thumbScrollY = mScrollbar.getThumbOffset().y; + int diffScrollY = scrollBarY - thumbScrollY; + if (diffScrollY * dy > 0f) { + // User is scrolling in the same direction the thumb needs to catch up to the + // current scroll position. We do this by mapping the difference in movement + // from the original scroll bar position to the difference in movement necessary + // in the detached thumb position to ensure that both speed towards the same + // position at either end of the list. + if (dy < 0) { + int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY); + thumbScrollY += Math.max(offset, diffScrollY); + } else { + int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) / + (float) (availableScrollBarHeight - scrollBarY)); + thumbScrollY += Math.min(offset, diffScrollY); + } + thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY)); + mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); + if (scrollBarY == thumbScrollY) { + mScrollbar.reattachThumbToScroll(); + } + } else { + // User is scrolling in an opposite direction to the direction that the thumb + // needs to catch up to the scroll position. Do nothing except for updating + // the scroll bar x to match the thumb width. + mScrollbar.setThumbOffset(scrollBarX, thumbScrollY); + } + } + } else { + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); + } } /** @@ -283,13 +403,13 @@ public class AllAppsRecyclerView extends BaseRecyclerView /** * Returns the current scroll state of the apps rows. */ - private void getCurScrollState(ScrollPositionState stateOut, - List<AlphabeticalAppsList.AdapterItem> items) { + protected void getCurScrollState(ScrollPositionState stateOut) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; stateOut.rowHeight = -1; // Return early if there are no items or we haven't been measured + List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); if (items.isEmpty() || mNumAppsPerRow == 0) { return; } @@ -324,4 +444,20 @@ public class AllAppsRecyclerView extends BaseRecyclerView return 0; } } + + /** + * Updates the bounds of the empty search background. + */ + private void updateEmptySearchBackgroundBounds() { + if (mEmptySearchBackground == null) { + return; + } + + // Center the empty search background on this new view bounds + int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2; + int y = mEmptySearchBackgroundTopOffset; + mEmptySearchBackground.setBounds(x, y, + x + mEmptySearchBackground.getIntrinsicWidth(), + y + mEmptySearchBackground.getIntrinsicHeight()); + } } diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 47241ce5d..dac0df12a 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -43,6 +43,11 @@ public class AlphabeticalAppsList { private static final boolean DEBUG = false; private static final boolean DEBUG_PREDICTIONS = false; + private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; + private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; + + private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; + /** * Info about a section in the alphabetic list */ @@ -81,8 +86,6 @@ public class AlphabeticalAppsList { public int position; // The type of this item public int viewType; - // The row that this item shows up on - public int rowIndex; /** Section & App properties */ // The section for this item @@ -94,6 +97,8 @@ public class AlphabeticalAppsList { public String sectionName = null; // The index of this app in the section public int sectionAppIndex = -1; + // The row that this item shows up on + public int rowIndex; // The index of this app in the row public int rowAppIndex; // The associated AppInfo for the app @@ -111,14 +116,14 @@ public class AlphabeticalAppsList { } public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, - int sectionAppIndex, AppInfo appInfo, int appIndex) { + int sectionAppIndex, AppInfo appInfo, int appIndex) { AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; return item; } public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, - int sectionAppIndex, AppInfo appInfo, int appIndex) { + int sectionAppIndex, AppInfo appInfo, int appIndex) { AdapterItem item = new AdapterItem(); item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; item.position = pos; @@ -129,6 +134,27 @@ public class AlphabeticalAppsList { item.appIndex = appIndex; return item; } + + public static AdapterItem asEmptySearch(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE; + item.position = pos; + return item; + } + + public static AdapterItem asDivider(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE; + item.position = pos; + return item; + } + + public static AdapterItem asMarketSearch(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE; + item.position = pos; + return item; + } } /** @@ -222,17 +248,17 @@ public class AlphabeticalAppsList { } /** - * Returns the number of applications in this list. + * Returns the number of rows of applications (not including predictions) */ - public int getSize() { - return mFilteredApps.size(); + public int getNumAppRows() { + return mNumAppRowsInAdapter; } /** - * Returns the number of rows of applications (not including predictions) + * Returns the number of applications in this list. */ - public int getNumAppRows() { - return mNumAppRowsInAdapter; + public int getNumFilteredApps() { + return mFilteredApps.size(); } /** @@ -457,6 +483,16 @@ public class AlphabeticalAppsList { mFilteredApps.add(info); } + // Append the search market item if we are currently searching + if (hasFilter()) { + if (hasNoFilteredResults()) { + mAdapterItems.add(AdapterItem.asEmptySearch(position++)); + } else { + mAdapterItems.add(AdapterItem.asDivider(position++)); + } + mAdapterItems.add(AdapterItem.asMarketSearch(position++)); + } + // Merge multiple sections together as requested by the merge strategy for this device mergeSections(); @@ -484,18 +520,36 @@ public class AlphabeticalAppsList { } mNumAppRowsInAdapter = rowIndex + 1; - // Pre-calculate all the fast scroller fractions based on the number of rows - float rowFraction = 1f / mNumAppRowsInAdapter; - for (FastScrollSectionInfo info : mFastScrollerSections) { - AdapterItem item = info.fastScrollToItem; - if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && - item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { - info.touchFraction = 0f; - continue; - } - - float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); - info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + // Pre-calculate all the fast scroller fractions + switch (mFastScrollDistributionMode) { + case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: + float rowFraction = 1f / mNumAppRowsInAdapter; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + + float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); + info.touchFraction = item.rowIndex * rowFraction + subRowFraction; + } + break; + case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: + float perSectionTouchFraction = 1f / mFastScrollerSections.size(); + float cumulativeTouchFraction = 0f; + for (FastScrollSectionInfo info : mFastScrollerSections) { + AdapterItem item = info.fastScrollToItem; + if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && + item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { + info.touchFraction = 0f; + continue; + } + info.touchFraction = cumulativeTouchFraction; + cumulativeTouchFraction += perSectionTouchFraction; + } + break; } } diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java index 83b920589..3169f842a 100644 --- a/src/com/android/launcher3/allapps/DefaultAppSearchController.java +++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java @@ -25,6 +25,7 @@ import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; +import com.android.launcher3.ExtendedEditText; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; @@ -54,7 +55,8 @@ final class DefaultAppSearchController extends AllAppsSearchBarController @Thunk View mSearchBarContainerView; private View mSearchButtonView; private View mDismissSearchButtonView; - @Thunk AllAppsSearchEditView mSearchBarEditView; + @Thunk + ExtendedEditText mSearchBarEditView; @Thunk AllAppsRecyclerView mAppsRecyclerView; @Thunk Runnable mFocusRecyclerViewRunnable = new Runnable() { @Override @@ -82,21 +84,23 @@ final class DefaultAppSearchController extends AllAppsSearchBarController mSearchBarContainerView = mSearchView.findViewById(R.id.search_container); mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button); mDismissSearchButtonView.setOnClickListener(this); - mSearchBarEditView = (AllAppsSearchEditView) + mSearchBarEditView = (ExtendedEditText) mSearchBarContainerView.findViewById(R.id.search_box_input); mSearchBarEditView.addTextChangedListener(this); mSearchBarEditView.setOnEditorActionListener(this); mSearchBarEditView.setOnBackKeyListener( - new AllAppsSearchEditView.OnBackKeyListener() { + new ExtendedEditText.OnBackKeyListener() { @Override - public void onBackKey() { + public boolean onBackKey() { // Only hide the search field if there is no query, or if there // are no filtered results String query = Utilities.trim( mSearchBarEditView.getEditableText().toString()); if (query.isEmpty() || mApps.hasNoFilteredResults()) { hideSearchField(true, mFocusRecyclerViewRunnable); + return true; } + return false; } }); return mSearchView; @@ -166,22 +170,24 @@ final class DefaultAppSearchController extends AllAppsSearchBarController return false; } // Skip if it's not the right action - if (actionId != EditorInfo.IME_ACTION_DONE) { + if (actionId != EditorInfo.IME_ACTION_SEARCH) { return false; } - // Skip if there isn't exactly one item - if (mApps.getSize() != 1) { + // Skip if there are more than one icon + if (mApps.getNumFilteredApps() > 1) { return false; } - // If there is exactly one icon, then quick-launch it + // Otherwise, find the first icon, or fallback to the search-market-view and launch it List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems(); for (int i = 0; i < items.size(); i++) { AlphabeticalAppsList.AdapterItem item = items.get(i); - if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) { - mAppsRecyclerView.getChildAt(i).performClick(); - mInputMethodManager.hideSoftInputFromWindow( - mContainerView.getWindowToken(), 0); - return true; + switch (item.viewType) { + case AllAppsGridAdapter.ICON_VIEW_TYPE: + case AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE: + mAppsRecyclerView.getChildAt(i).performClick(); + mInputMethodManager.hideSoftInputFromWindow( + mContainerView.getWindowToken(), 0); + return true; } } return false; diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java index 7aa36d447..434f13dcc 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompat.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompat.java @@ -40,7 +40,7 @@ public abstract class AppWidgetManagerCompat { public static AppWidgetManagerCompat getInstance(Context context) { synchronized (sInstanceLock) { if (sInstance == null) { - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { sInstance = new AppWidgetManagerCompatVL(context.getApplicationContext()); } else { sInstance = new AppWidgetManagerCompatV16(context.getApplicationContext()); diff --git a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java index f7f4b7e4f..463cf902d 100644 --- a/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java +++ b/src/com/android/launcher3/compat/AppWidgetManagerCompatV16.java @@ -54,10 +54,10 @@ class AppWidgetManagerCompatV16 extends AppWidgetManagerCompat { @Override public boolean bindAppWidgetIdIfAllowed(int appWidgetId, AppWidgetProviderInfo info, Bundle options) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { - return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider); - } else { + if (Utilities.ATLEAST_JB_MR1) { return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider, options); + } else { + return mAppWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, info.provider); } } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java index 5858bc8b9..95e3ba902 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompat.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java @@ -53,7 +53,7 @@ public abstract class LauncherAppsCompat { public static LauncherAppsCompat getInstance(Context context) { synchronized (sInstanceLock) { if (sInstance == null) { - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { sInstance = new LauncherAppsCompatVL(context.getApplicationContext()); } else { sInstance = new LauncherAppsCompatV16(context.getApplicationContext()); diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java index ac3d252f5..339c457e1 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java @@ -31,6 +31,7 @@ import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import com.android.launcher3.Utilities; import com.android.launcher3.util.Thunk; import java.util.ArrayList; @@ -188,7 +189,7 @@ public class LauncherAppsCompatV16 extends LauncherAppsCompat { // when moving a package or mounting/un-mounting external storage. Assume that // it is a replacing operation. final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, - Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT); + !Utilities.ATLEAST_KITKAT); String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); for (OnAppsChangedCallbackCompat callback : getCallbacks()) { callback.onPackagesAvailable(packages, user, replacing); diff --git a/src/com/android/launcher3/compat/PackageInstallerCompat.java b/src/com/android/launcher3/compat/PackageInstallerCompat.java index c49908328..ec5014d7c 100644 --- a/src/com/android/launcher3/compat/PackageInstallerCompat.java +++ b/src/com/android/launcher3/compat/PackageInstallerCompat.java @@ -34,7 +34,7 @@ public abstract class PackageInstallerCompat { public static PackageInstallerCompat getInstance(Context context) { synchronized (sInstanceLock) { if (sInstance == null) { - if (Utilities.isLmpOrAbove()) { + if (Utilities.ATLEAST_LOLLIPOP) { sInstance = new PackageInstallerCompatVL(context); } else { sInstance = new PackageInstallerCompatV16(); diff --git a/src/com/android/launcher3/compat/UserHandleCompat.java b/src/com/android/launcher3/compat/UserHandleCompat.java index ab4b7216b..567022b43 100644 --- a/src/com/android/launcher3/compat/UserHandleCompat.java +++ b/src/com/android/launcher3/compat/UserHandleCompat.java @@ -34,7 +34,7 @@ public class UserHandleCompat { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static UserHandleCompat myUserHandle() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { return new UserHandleCompat(android.os.Process.myUserHandle()); } else { return new UserHandleCompat(); @@ -55,7 +55,7 @@ public class UserHandleCompat { @Override public String toString() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { return mUser.toString(); } else { return ""; @@ -67,7 +67,7 @@ public class UserHandleCompat { if (!(other instanceof UserHandleCompat)) { return false; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { return mUser.equals(((UserHandleCompat) other).mUser); } else { return true; @@ -76,7 +76,7 @@ public class UserHandleCompat { @Override public int hashCode() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { return mUser.hashCode(); } else { return 0; @@ -89,7 +89,7 @@ public class UserHandleCompat { * profiles so this is a no-op. */ public void addToIntent(Intent intent, String name) { - if (Utilities.isLmpOrAbove() && mUser != null) { + if (Utilities.ATLEAST_LOLLIPOP && mUser != null) { intent.putExtra(name, mUser); } } diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java index a79d94646..f708004a3 100644 --- a/src/com/android/launcher3/compat/UserManagerCompat.java +++ b/src/com/android/launcher3/compat/UserManagerCompat.java @@ -28,16 +28,29 @@ public abstract class UserManagerCompat { protected UserManagerCompat() { } + private static final Object sInstanceLock = new Object(); + private static UserManagerCompat sInstance; + public static UserManagerCompat getInstance(Context context) { - if (Utilities.isLmpOrAbove()) { - return new UserManagerCompatVL(context); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return new UserManagerCompatV17(context); - } else { - return new UserManagerCompatV16(); + synchronized (sInstanceLock) { + if (sInstance == null) { + if (Utilities.ATLEAST_LOLLIPOP) { + sInstance = new UserManagerCompatVL(context.getApplicationContext()); + } else if (Utilities.ATLEAST_JB_MR1) { + sInstance = new UserManagerCompatV17(context.getApplicationContext()); + } else { + sInstance = new UserManagerCompatV16(); + } + } + return sInstance; } } + /** + * Creates a cache for users. + */ + public abstract void enableAndResetCache(); + public abstract List<UserHandleCompat> getUserProfiles(); public abstract long getSerialNumberForUser(UserHandleCompat user); public abstract UserHandleCompat getUserForSerialNumber(long serialNumber); diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java index ffe698c8b..85aee57e8 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV16.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java @@ -53,4 +53,8 @@ public class UserManagerCompatV16 extends UserManagerCompat { public long getUserCreationTime(UserHandleCompat user) { return 0; } + + @Override + public void enableAndResetCache() { + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatV17.java b/src/com/android/launcher3/compat/UserManagerCompatV17.java index c42c00c7d..75203b7f3 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatV17.java +++ b/src/com/android/launcher3/compat/UserManagerCompatV17.java @@ -21,8 +21,18 @@ import android.content.Context; import android.os.Build; import android.os.UserManager; +import com.android.launcher3.util.LongArrayMap; + +import java.util.HashMap; + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public class UserManagerCompatV17 extends UserManagerCompatV16 { + + protected LongArrayMap<UserHandleCompat> mUsers; + // Create a separate reverse map as LongArrayMap.indexOfValue checks if objects are same + // and not {@link Object#equals} + protected HashMap<UserHandleCompat, Long> mUserToSerialMap; + protected UserManager mUserManager; UserManagerCompatV17(Context context) { @@ -30,11 +40,34 @@ public class UserManagerCompatV17 extends UserManagerCompatV16 { } public long getSerialNumberForUser(UserHandleCompat user) { + synchronized (this) { + if (mUserToSerialMap != null) { + Long serial = mUserToSerialMap.get(user); + return serial == null ? 0 : serial; + } + } return mUserManager.getSerialNumberForUser(user.getUser()); } public UserHandleCompat getUserForSerialNumber(long serialNumber) { + synchronized (this) { + if (mUsers != null) { + return mUsers.get(serialNumber); + } + } return UserHandleCompat.fromUser(mUserManager.getUserForSerialNumber(serialNumber)); } + + @Override + public void enableAndResetCache() { + synchronized (this) { + mUsers = new LongArrayMap<>(); + mUserToSerialMap = new HashMap<>(); + UserHandleCompat myUser = UserHandleCompat.myUserHandle(); + long serial = mUserManager.getSerialNumberForUser(myUser.getUser()); + mUsers.put(serial, myUser); + mUserToSerialMap.put(myUser, serial); + } + } } diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index dd7a72617..dc3ec3cd8 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -24,9 +24,14 @@ import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.UserHandle; + import com.android.launcher3.LauncherAppState; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.LongArrayMap; + import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -43,7 +48,32 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { } @Override + public void enableAndResetCache() { + synchronized (this) { + mUsers = new LongArrayMap<>(); + mUserToSerialMap = new HashMap<>(); + List<UserHandle> users = mUserManager.getUserProfiles(); + if (users != null) { + for (UserHandle user : users) { + long serial = mUserManager.getSerialNumberForUser(user); + UserHandleCompat userCompat = UserHandleCompat.fromUser(user); + mUsers.put(serial, userCompat); + mUserToSerialMap.put(userCompat, serial); + } + } + } + } + + @Override public List<UserHandleCompat> getUserProfiles() { + synchronized (this) { + if (mUsers != null) { + List<UserHandleCompat> users = new ArrayList<>(); + users.addAll(mUserToSerialMap.keySet()); + return users; + } + } + List<UserHandle> users = mUserManager.getUserProfiles(); if (users == null) { return Collections.emptyList(); @@ -71,7 +101,9 @@ public class UserManagerCompatVL extends UserManagerCompatV17 { @Override public long getUserCreationTime(UserHandleCompat user) { - // TODO: Use system API once available. + if (Utilities.ATLEAST_MARSHMALLOW) { + return mUserManager.getUserCreationTime(user.getUser()); + } SharedPreferences prefs = mContext.getSharedPreferences( LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE); String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user); diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java index cf47ce648..bd28560f3 100644 --- a/src/com/android/launcher3/model/AbstractUserComparator.java +++ b/src/com/android/launcher3/model/AbstractUserComparator.java @@ -22,14 +22,12 @@ import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import java.util.Comparator; -import java.util.HashMap; /** * A comparator to arrange items based on user profiles. */ public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> { - private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>(); private final UserManagerCompat mUserManager; private final UserHandleCompat mMyUser; @@ -43,25 +41,9 @@ public abstract class AbstractUserComparator<T extends ItemInfo> implements Comp if (mMyUser.equals(lhs.user)) { return -1; } else { - Long aUserSerial = getAndCacheUserSerial(lhs.user); - Long bUserSerial = getAndCacheUserSerial(rhs.user); + Long aUserSerial = mUserManager.getSerialNumberForUser(lhs.user); + Long bUserSerial = mUserManager.getSerialNumberForUser(rhs.user); return aUserSerial.compareTo(bUserSerial); } } - - /** - * Returns the user serial for this user, using a cached serial if possible. - */ - private Long getAndCacheUserSerial(UserHandleCompat user) { - Long userSerial = mUserSerialCache.get(user); - if (userSerial == null) { - userSerial = mUserManager.getSerialNumberForUser(user); - mUserSerialCache.put(user, userSerial); - } - return userSerial; - } - - public void clearUserCache() { - mUserSerialCache.clear(); - } } diff --git a/src/com/android/launcher3/model/AppNameComparator.java b/src/com/android/launcher3/model/AppNameComparator.java index c4b74d4dc..5f80037dc 100644 --- a/src/com/android/launcher3/model/AppNameComparator.java +++ b/src/com/android/launcher3/model/AppNameComparator.java @@ -68,8 +68,6 @@ public class AppNameComparator { * Returns a locale-aware comparator that will alphabetically order a list of applications. */ public Comparator<ItemInfo> getAppInfoComparator() { - // Clear the user serial cache so that we get serials as needed in the comparator - mAppInfoComparator.clearUserCache(); return mAppInfoComparator; } diff --git a/src/com/android/launcher3/model/MigrateFromRestoreTask.java b/src/com/android/launcher3/model/MigrateFromRestoreTask.java new file mode 100644 index 000000000..6a529f61f --- /dev/null +++ b/src/com/android/launcher3/model/MigrateFromRestoreTask.java @@ -0,0 +1,767 @@ +package com.android.launcher3.model; + +import android.content.ComponentName; +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.graphics.Point; +import android.text.TextUtils; +import android.util.Log; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.LauncherProvider; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.LauncherSettings.Favorites; +import com.android.launcher3.Utilities; +import com.android.launcher3.compat.PackageInstallerCompat; +import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.Thunk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class takes care of shrinking the workspace (by maximum of one row and one column), as a + * result of restoring from a larger device. + */ +public class MigrateFromRestoreTask { + + public static boolean ENABLED = false; + + private static final String TAG = "MigrateFromRestoreTask"; + private static final boolean DEBUG = true; + + private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size"; + private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size"; + + // These are carefully selected weights for various item types (Math.random?), to allow for + // the lease absurd migration experience. + private static final float WT_SHORTCUT = 1; + private static final float WT_APPLICATION = 0.8f; + private static final float WT_WIDGET_MIN = 2; + private static final float WT_WIDGET_FACTOR = 0.6f; + private static final float WT_FOLDER_FACTOR = 0.5f; + + private final Context mContext; + private final ContentValues mTempValues = new ContentValues(); + private final HashMap<String, Point> mWidgetMinSize; + private final InvariantDeviceProfile mIdp; + + private HashSet<String> mValidPackages; + public ArrayList<Long> mEntryToRemove; + private ArrayList<ContentProviderOperation> mUpdateOperations; + + private ArrayList<DbEntry> mCarryOver; + + private final int mSrcX, mSrcY; + @Thunk final int mTrgX, mTrgY; + private final boolean mShouldRemoveX, mShouldRemoveY; + + public MigrateFromRestoreTask(Context context) { + mContext = context; + + SharedPreferences prefs = prefs(context); + Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, "")); + mSrcX = sourceSize.x; + mSrcY = sourceSize.y; + + mWidgetMinSize = new HashMap<String, Point>(); + for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, + Collections.<String>emptySet())) { + String[] parts = s.split("#"); + mWidgetMinSize.put(parts[0], parsePoint(parts[1])); + } + + mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + mTrgX = mIdp.numColumns; + mTrgY = mIdp.numRows; + mShouldRemoveX = mTrgX < mSrcX; + mShouldRemoveY = mTrgY < mSrcY; + } + + public void execute() throws Exception { + mEntryToRemove = new ArrayList<>(); + mCarryOver = new ArrayList<>(); + mUpdateOperations = new ArrayList<>(); + + // Initialize list of valid packages. This contain all the packages which are already on + // the device and packages which are being installed. Any item which doesn't belong to + // this set is removed. + // Since the loader removes such items anyway, removing these items here doesn't cause any + // extra data loss and gives us more free space on the grid for better migration. + mValidPackages = new HashSet<>(); + for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) { + mValidPackages.add(info.packageName); + } + mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext) + .updateAndGetActiveSessionCache().keySet()); + + ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + if (allScreens.isEmpty()) { + throw new Exception("Unable to get workspace screens"); + } + + for (long screenId : allScreens) { + if (DEBUG) { + Log.d(TAG, "Migrating " + screenId); + } + migrateScreen(screenId); + } + + if (!mCarryOver.isEmpty()) { + LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); + for (DbEntry e : mCarryOver) { + itemMap.put(e.id, e); + } + + do { + // Some items are still remaining. Try adding a few new screens. + + // At every iteration, make sure that at least one item is removed from + // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed, + // break the loop and abort migration by throwing an exception. + OptimalPlacementSolution placement = new OptimalPlacementSolution( + new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true); + placement.find(); + if (placement.finalPlacedItems.size() > 0) { + long newScreenId = LauncherAppState.getLauncherProvider().generateNewScreenId(); + allScreens.add(newScreenId); + for (DbEntry item : placement.finalPlacedItems) { + if (!mCarryOver.remove(itemMap.get(item.id))) { + throw new Exception("Unable to find matching items"); + } + item.screenId = newScreenId; + update(item); + } + } else { + throw new Exception("None of the items can be placed on an empty screen"); + } + + } while (!mCarryOver.isEmpty()); + + + LauncherAppState.getInstance().getModel() + .updateWorkspaceScreenOrder(mContext, allScreens); + } + + // Update items + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); + + if (!mEntryToRemove.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + } + mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, mEntryToRemove), null); + } + + // Make sure we haven't removed everything. + final Cursor c = mContext.getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); + boolean hasData = c.moveToNext(); + c.close(); + if (!hasData) { + throw new Exception("Removed every thing during grid resize"); + } + } + + /** + * Migrate a particular screen id. + * Strategy: + * 1) For all possible combinations of row and column, pick the one which causes the least + * data loss: {@link #tryRemove(int, int, ArrayList, float[])} + * 2) Maintain a list of all lost items before this screen, and add any new item lost from + * this screen to that list as well. + * 3) If all those items from the above list can be placed on this screen, place them + * (otherwise they are placed on a new screen). + */ + private void migrateScreen(long screenId) { + ArrayList<DbEntry> items = loadEntries(screenId); + + int removedCol = Integer.MAX_VALUE; + int removedRow = Integer.MAX_VALUE; + + // removeWt represents the cost function for loss of items during migration, and moveWt + // represents the cost function for repositioning the items. moveWt is only considered if + // removeWt is same for two different configurations. + // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least + // cost. + float removeWt = Float.MAX_VALUE; + float moveWt = Float.MAX_VALUE; + float[] outLoss = new float[2]; + ArrayList<DbEntry> finalItems = null; + + // Try removing all possible combinations + for (int x = 0; x < mSrcX; x++) { + for (int y = 0; y < mSrcY; y++) { + // Use a deep copy when trying out a particular combination as it can change + // the underlying object. + ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss); + + if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) { + removeWt = outLoss[0]; + moveWt = outLoss[1]; + removedCol = mShouldRemoveX ? x : removedCol; + removedRow = mShouldRemoveY ? y : removedRow; + finalItems = itemsOnScreen; + } + + // No need to loop over all rows, if a row removal is not needed. + if (!mShouldRemoveY) { + break; + } + } + + if (!mShouldRemoveX) { + break; + } + } + + if (DEBUG) { + Log.d(TAG, String.format("Removing row %d, column %d on screen %d", + removedRow, removedCol, screenId)); + } + + LongArrayMap<DbEntry> itemMap = new LongArrayMap<>(); + for (DbEntry e : deepCopy(items)) { + itemMap.put(e.id, e); + } + + for (DbEntry item : finalItems) { + DbEntry org = itemMap.get(item.id); + itemMap.remove(item.id); + + // Check if update is required + if (!item.columnsSame(org)) { + update(item); + } + } + + // The remaining items in {@link #itemMap} are those which didn't get placed. + for (DbEntry item : itemMap) { + mCarryOver.add(item); + } + + if (!mCarryOver.isEmpty() && removeWt == 0) { + // No new items were removed in this step. Try placing all the items on this screen. + boolean[][] occupied = new boolean[mTrgX][mTrgY]; + for (DbEntry item : finalItems) { + markCells(occupied, item, true); + } + + OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, + deepCopy(mCarryOver), true); + placement.find(); + if (placement.lowestWeightLoss == 0) { + // All items got placed + + for (DbEntry item : placement.finalPlacedItems) { + item.screenId = screenId; + update(item); + } + + mCarryOver.clear(); + } + } + } + + /** + * Updates an item in the DB. + */ + private void update(DbEntry item) { + mTempValues.clear(); + item.addToContentValues(mTempValues); + mUpdateOperations.add(ContentProviderOperation + .newUpdate(LauncherSettings.Favorites.getContentUri(item.id)) + .withValues(mTempValues).build()); + } + + /** + * Tries the remove the provided row and column. + * @param items all the items on the screen under operation + * @param outLoss array of size 2. The first entry is filled with weight loss, and the second + * with the overall item movement. + */ + private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items, + float[] outLoss) { + boolean[][] occupied = new boolean[mTrgX][mTrgY]; + + col = mShouldRemoveX ? col : Integer.MAX_VALUE; + row = mShouldRemoveY ? row : Integer.MAX_VALUE; + + ArrayList<DbEntry> finalItems = new ArrayList<>(); + ArrayList<DbEntry> removedItems = new ArrayList<>(); + + for (DbEntry item : items) { + if ((item.cellX <= col && (item.spanX + item.cellX) > col) + || (item.cellY <= row && (item.spanY + item.cellY) > row)) { + removedItems.add(item); + if (item.cellX >= col) item.cellX --; + if (item.cellY >= row) item.cellY --; + } else { + if (item.cellX > col) item.cellX --; + if (item.cellY > row) item.cellY --; + finalItems.add(item); + markCells(occupied, item, true); + } + } + + OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems); + placement.find(); + finalItems.addAll(placement.finalPlacedItems); + outLoss[0] = placement.lowestWeightLoss; + outLoss[1] = placement.lowestMoveCost; + return finalItems; + } + + @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) { + for (int i = item.cellX; i < (item.cellX + item.spanX); i++) { + for (int j = item.cellY; j < (item.cellY + item.spanY); j++) { + occupied[i][j] = val; + } + } + } + + @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { + if (x + w > mTrgX) return false; + if (y + h > mTrgY) return false; + + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + if (occupied[i + x][j + y]) { + return false; + } + } + } + return true; + } + + private class OptimalPlacementSolution { + private final ArrayList<DbEntry> itemsToPlace; + private final boolean[][] occupied; + + // If set to true, item movement are not considered in move cost, leading to a more + // linear placement. + private final boolean ignoreMove; + + float lowestWeightLoss = Float.MAX_VALUE; + float lowestMoveCost = Float.MAX_VALUE; + ArrayList<DbEntry> finalPlacedItems; + + public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) { + this(occupied, itemsToPlace, false); + } + + public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace, + boolean ignoreMove) { + this.occupied = occupied; + this.itemsToPlace = itemsToPlace; + this.ignoreMove = ignoreMove; + + // Sort the items such that larger widgets appear first followed by 1x1 items + Collections.sort(this.itemsToPlace); + } + + public void find() { + find(0, 0, 0, new ArrayList<DbEntry>()); + } + + /** + * Recursively finds a placement for the provided items. + * @param index the position in {@link #itemsToPlace} to start looking at. + * @param weightLoss total weight loss upto this point + * @param moveCost total move cost upto this point + * @param itemsPlaced all the items already placed upto this point + */ + public void find(int index, float weightLoss, float moveCost, + ArrayList<DbEntry> itemsPlaced) { + if ((weightLoss >= lowestWeightLoss) || + ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) { + // Abort, as we already have a better solution. + return; + + } else if (index >= itemsToPlace.size()) { + // End loop. + lowestWeightLoss = weightLoss; + lowestMoveCost = moveCost; + + // Keep a deep copy of current configuration as it can change during recursion. + finalPlacedItems = deepCopy(itemsPlaced); + return; + } + + DbEntry me = itemsToPlace.get(index); + int myX = me.cellX; + int myY = me.cellY; + + // List of items to pass over if this item was placed. + ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1); + itemsIncludingMe.addAll(itemsPlaced); + itemsIncludingMe.add(me); + + if (me.spanX > 1 || me.spanY > 1) { + // If the current item is a widget (and it greater than 1x1), try to place it at + // all possible positions. This is because a widget placed at one position can + // affect the placement of a different widget. + int myW = me.spanX; + int myH = me.spanY; + + for (int y = 0; y < mTrgY; y++) { + for (int x = 0; x < mTrgX; x++) { + float newMoveCost = moveCost; + if (x != myX) { + me.cellX = x; + newMoveCost ++; + } + if (y != myY) { + me.cellY = y; + newMoveCost ++; + } + if (ignoreMove) { + newMoveCost = moveCost; + } + + if (isVacant(occupied, x, y, myW, myH)) { + // place at this position and continue search. + markCells(occupied, me, true); + find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); + markCells(occupied, me, false); + } + + // Try resizing horizontally + if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) { + me.spanX --; + markCells(occupied, me, true); + // 1 extra move cost + find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); + markCells(occupied, me, false); + me.spanX ++; + } + + // Try resizing vertically + if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) { + me.spanY --; + markCells(occupied, me, true); + // 1 extra move cost + find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe); + markCells(occupied, me, false); + me.spanY ++; + } + + // Try resizing horizontally & vertically + if (myH > me.minSpanY && myW > me.minSpanX && + isVacant(occupied, x, y, myW - 1, myH - 1)) { + me.spanX --; + me.spanY --; + markCells(occupied, me, true); + // 2 extra move cost + find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe); + markCells(occupied, me, false); + me.spanX ++; + me.spanY ++; + } + me.cellX = myX; + me.cellY = myY; + } + } + + // Finally also try a solution when this item is not included. Trying it in the end + // causes it to get skipped in most cases due to higher weight loss, and prevents + // unnecessary deep copies of various configurations. + find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); + } else { + // Since this is a 1x1 item and all the following items are also 1x1, just place + // it at 'the most appropriate position' and hope for the best. + // The most appropriate position: one with lease straight line distance + int newDistance = Integer.MAX_VALUE; + int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE; + + for (int y = 0; y < mTrgY; y++) { + for (int x = 0; x < mTrgX; x++) { + if (!occupied[x][y]) { + int dist = ignoreMove ? 0 : + ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y)); + if (dist < newDistance) { + newX = x; + newY = y; + newDistance = dist; + } + } + } + } + + if (newX < mTrgX && newY < mTrgY) { + float newMoveCost = moveCost; + if (newX != myX) { + me.cellX = newX; + newMoveCost ++; + } + if (newY != myY) { + me.cellY = newY; + newMoveCost ++; + } + if (ignoreMove) { + newMoveCost = moveCost; + } + markCells(occupied, me, true); + find(index + 1, weightLoss, newMoveCost, itemsIncludingMe); + markCells(occupied, me, false); + me.cellX = myX; + me.cellY = myY; + + // Try to find a solution without this item, only if + // 1) there was at least one space, i.e., we were able to place this item + // 2) if the next item has the same weight (all items are already sorted), as + // if it has lower weight, that solution will automatically get discarded. + // 3) ignoreMove false otherwise, move cost is ignored and the weight will + // anyway be same. + if (index + 1 < itemsToPlace.size() + && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) { + find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced); + } + } else { + // No more space. Jump to the end. + for (int i = index + 1; i < itemsToPlace.size(); i++) { + weightLoss += itemsToPlace.get(i).weight; + } + find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced); + } + } + } + } + + /** + * Loads entries for a particular screen id. + */ + public ArrayList<DbEntry> loadEntries(long screen) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[] { + Favorites._ID, // 0 + Favorites.ITEM_TYPE, // 1 + Favorites.CELLX, // 2 + Favorites.CELLY, // 3 + Favorites.SPANX, // 4 + Favorites.SPANY, // 5 + Favorites.INTENT, // 6 + Favorites.APPWIDGET_PROVIDER}, // 7 + Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP + + " AND " + Favorites.SCREEN + " = " + screen, null, null, null); + + final int indexId = c.getColumnIndexOrThrow(Favorites._ID); + final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX); + final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY); + final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX); + final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY); + final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); + final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER); + + ArrayList<DbEntry> entries = new ArrayList<>(); + while (c.moveToNext()) { + DbEntry entry = new DbEntry(); + entry.id = c.getLong(indexId); + entry.itemType = c.getInt(indexItemType); + entry.cellX = c.getInt(indexCellX); + entry.cellY = c.getInt(indexCellY); + entry.spanX = c.getInt(indexSpanX); + entry.spanY = c.getInt(indexSpanY); + entry.screenId = screen; + + try { + // calculate weight + switch (entry.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + verifyIntent(c.getString(indexIntent)); + entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT + ? WT_SHORTCUT : WT_APPLICATION; + break; + } + case Favorites.ITEM_TYPE_APPWIDGET: { + String provider = c.getString(indexAppWidgetProvider); + ComponentName cn = ComponentName.unflattenFromString(provider); + verifyPackage(cn.getPackageName()); + entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR + * entry.spanX * entry.spanY); + + // Migration happens for current user only. + LauncherAppWidgetProviderInfo pInfo = LauncherModel.getProviderInfo( + mContext, cn, UserHandleCompat.myUserHandle()); + Point spans = pInfo == null ? + mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext); + if (spans != null) { + entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX; + entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY; + } else { + // Assume that the widget be resized down to 2x2 + entry.minSpanX = entry.minSpanY = 2; + } + + if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) { + throw new Exception("Widget can't be resized down to fit the grid"); + } + break; + } + case Favorites.ITEM_TYPE_FOLDER: { + int total = getFolderItemsCount(entry.id); + if (total == 0) { + throw new Exception("Folder is empty"); + } + entry.weight = WT_FOLDER_FACTOR * total; + break; + } + default: + throw new Exception("Invalid item type"); + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Removing item " + entry.id, e); + } + mEntryToRemove.add(entry.id); + continue; + } + + entries.add(entry); + } + return entries; + } + + /** + * @return the number of valid items in the folder. + */ + private int getFolderItemsCount(long folderId) { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[] {Favorites._ID, Favorites.INTENT}, + Favorites.CONTAINER + " = " + folderId, null, null, null); + + int total = 0; + while (c.moveToNext()) { + try { + verifyIntent(c.getString(1)); + total++; + } catch (Exception e) { + mEntryToRemove.add(c.getLong(0)); + } + } + + return total; + } + + /** + * Verifies if the intent should be restored. + */ + private void verifyIntent(String intentStr) throws Exception { + Intent intent = Intent.parseUri(intentStr, 0); + if (intent.getComponent() != null) { + verifyPackage(intent.getComponent().getPackageName()); + } else if (intent.getPackage() != null) { + // Only verify package if the component was null. + verifyPackage(intent.getPackage()); + } + } + + /** + * Verifies if the package should be restored + */ + private void verifyPackage(String packageName) throws Exception { + if (!mValidPackages.contains(packageName)) { + throw new Exception("Package not available"); + } + } + + private static class DbEntry extends ItemInfo implements Comparable<DbEntry> { + + public float weight; + + public DbEntry() { } + + public DbEntry copy() { + DbEntry entry = new DbEntry(); + entry.copyFrom(this); + entry.weight = weight; + entry.minSpanX = minSpanX; + entry.minSpanY = minSpanY; + return entry; + } + + /** + * Comparator such that larger widgets come first, followed by all 1x1 items + * based on their weights. + */ + @Override + public int compareTo(DbEntry another) { + if (itemType == Favorites.ITEM_TYPE_APPWIDGET) { + if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { + return another.spanY * another.spanX - spanX * spanY; + } else { + return -1; + } + } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) { + return 1; + } else { + // Place higher weight before lower weight. + return Float.compare(another.weight, weight); + } + } + + public boolean columnsSame(DbEntry org) { + return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX && + org.spanY == spanY && org.screenId == screenId; + } + + public void addToContentValues(ContentValues values) { + values.put(LauncherSettings.Favorites.SCREEN, screenId); + values.put(LauncherSettings.Favorites.CELLX, cellX); + values.put(LauncherSettings.Favorites.CELLY, cellY); + values.put(LauncherSettings.Favorites.SPANX, spanX); + values.put(LauncherSettings.Favorites.SPANY, spanY); + } + } + + @Thunk static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) { + ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size()); + for (DbEntry e : src) { + dup.add(e.copy()); + } + return dup; + } + + private static Point parsePoint(String point) { + String[] split = point.split(","); + return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1])); + } + + public static void markForMigration(Context context, int srcX, int srcY, + HashSet<String> widgets) { + prefs(context).edit() + .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY) + .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets) + .apply(); + } + + public static boolean shouldRunTask(Context context) { + return !TextUtils.isEmpty(prefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, "")); + } + + public static void clearFlags(Context context) { + prefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE) + .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); + } + + private static SharedPreferences prefs(Context context) { + return context.getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), + Context.MODE_PRIVATE); + } +} diff --git a/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java index 61e895283..b99056023 100644 --- a/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java +++ b/src/com/android/launcher3/model/WidgetsAndShortcutNameComparator.java @@ -1,13 +1,14 @@ package com.android.launcher3.model; +import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; - import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.UserHandleCompat; +import com.android.launcher3.util.ComponentKey; import java.text.Collator; import java.util.Comparator; @@ -16,53 +17,81 @@ import java.util.HashMap; public class WidgetsAndShortcutNameComparator implements Comparator<Object> { private final AppWidgetManagerCompat mManager; private final PackageManager mPackageManager; - private final HashMap<Object, String> mLabelCache; + private final HashMap<ComponentKey, String> mLabelCache; private final Collator mCollator; private final UserHandleCompat mMainHandle; public WidgetsAndShortcutNameComparator(Context context) { mManager = AppWidgetManagerCompat.getInstance(context); mPackageManager = context.getPackageManager(); - mLabelCache = new HashMap<Object, String>(); + mLabelCache = new HashMap<>(); mCollator = Collator.getInstance(); mMainHandle = UserHandleCompat.myUserHandle(); } - @Override - public final int compare(Object a, Object b) { - String labelA, labelB; - if (mLabelCache.containsKey(a)) { - labelA = mLabelCache.get(a); - } else { - labelA = (a instanceof LauncherAppWidgetProviderInfo) - ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) a)) - : Utilities.trim(((ResolveInfo) a).loadLabel(mPackageManager)); - mLabelCache.put(a, labelA); - } - if (mLabelCache.containsKey(b)) { - labelB = mLabelCache.get(b); - } else { - labelB = (b instanceof LauncherAppWidgetProviderInfo) - ? Utilities.trim(mManager.loadLabel((LauncherAppWidgetProviderInfo) b)) - : Utilities.trim(((ResolveInfo) b).loadLabel(mPackageManager)); - mLabelCache.put(b, labelB); - } - - // Currently, there is no work profile shortcuts, hence only considering the widget cases. + /** + * Resets any stored state. + */ + public void reset() { + mLabelCache.clear(); + } - boolean aWorkProfile = (a instanceof LauncherAppWidgetProviderInfo) && - !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) a)); - boolean bWorkProfile = (b instanceof LauncherAppWidgetProviderInfo) && - !mMainHandle.equals(mManager.getUser((LauncherAppWidgetProviderInfo) b)); + @Override + public final int compare(Object objA, Object objB) { + ComponentKey keyA = getComponentKey(objA); + ComponentKey keyB = getComponentKey(objB); // Independent of how the labels compare, if only one of the two widget info belongs to // work profile, put that one in the back. + boolean aWorkProfile = !mMainHandle.equals(keyA.user); + boolean bWorkProfile = !mMainHandle.equals(keyB.user); if (aWorkProfile && !bWorkProfile) { return 1; } if (!aWorkProfile && bWorkProfile) { return -1; } + + // Get the labels for comparison + String labelA = mLabelCache.get(keyA); + String labelB = mLabelCache.get(keyB); + if (labelA == null) { + labelA = getLabel(objA); + mLabelCache.put(keyA, labelA); + } + if (labelB == null) { + labelB = getLabel(objB); + mLabelCache.put(keyB, labelB); + } return mCollator.compare(labelA, labelB); } + + /** + * @return a component key for the given widget or shortcut info. + */ + private ComponentKey getComponentKey(Object o) { + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + return new ComponentKey(widgetInfo.provider, mManager.getUser(widgetInfo)); + } else { + ResolveInfo shortcutInfo = (ResolveInfo) o; + ComponentName cn = new ComponentName(shortcutInfo.activityInfo.packageName, + shortcutInfo.activityInfo.name); + // Currently, there are no work profile shortcuts + return new ComponentKey(cn, UserHandleCompat.myUserHandle()); + } + } + + /** + * @return the label for the given widget or shortcut info. This may be an expensive call. + */ + private String getLabel(Object o) { + if (o instanceof LauncherAppWidgetProviderInfo) { + LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; + return Utilities.trim(mManager.loadLabel(widgetInfo)); + } else { + ResolveInfo shortcutInfo = (ResolveInfo) o; + return Utilities.trim(shortcutInfo.loadLabel(mPackageManager)); + } + } }; diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java index 185dfcae3..eef4f9173 100644 --- a/src/com/android/launcher3/model/WidgetsModel.java +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -8,6 +8,9 @@ import android.util.Log; import com.android.launcher3.AppFilter; import com.android.launcher3.IconCache; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Utilities; import com.android.launcher3.compat.AlphabeticIndexCompat; @@ -39,8 +42,8 @@ public class WidgetsModel { private ArrayList<Object> mRawList; private final AppWidgetManagerCompat mAppWidgetMgr; - private final Comparator mWidgetAndShortcutNameComparator; - private final Comparator mAppNameComparator; + private final WidgetsAndShortcutNameComparator mWidgetAndShortcutNameComparator; + private final Comparator<ItemInfo> mAppNameComparator; private final IconCache mIconCache; private final AppFilter mAppFilter; private AlphabeticIndexCompat mIndexer; @@ -54,6 +57,7 @@ public class WidgetsModel { mIndexer = new AlphabeticIndexCompat(context); } + @SuppressWarnings("unchecked") private WidgetsModel(WidgetsModel model) { mAppWidgetMgr = model.mAppWidgetMgr; mPackageItemInfos = (ArrayList<PackageItemInfo>) model.mPackageItemInfos.clone(); @@ -103,6 +107,9 @@ public class WidgetsModel { // clear the lists. mWidgetsList.clear(); mPackageItemInfos.clear(); + mWidgetAndShortcutNameComparator.reset(); + + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); // add and update. for (Object o: rawWidgetsShortcuts) { @@ -111,9 +118,23 @@ public class WidgetsModel { ComponentName componentName = null; if (o instanceof LauncherAppWidgetProviderInfo) { LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o; - componentName = widgetInfo.provider; - packageName = widgetInfo.provider.getPackageName(); - userHandle = mAppWidgetMgr.getUser(widgetInfo); + + // Ensure that all widgets we show can be added on a workspace of this size + int minSpanX = Math.min(widgetInfo.spanX, widgetInfo.minSpanX); + int minSpanY = Math.min(widgetInfo.spanY, widgetInfo.minSpanY); + if (minSpanX <= (int) idp.numColumns && + minSpanY <= (int) idp.numRows) { + componentName = widgetInfo.provider; + packageName = widgetInfo.provider.getPackageName(); + userHandle = mAppWidgetMgr.getUser(widgetInfo); + } else { + if (DEBUG) { + Log.d(TAG, String.format( + "Widget %s : (%d X %d) can't fit on this device", + widgetInfo.provider, minSpanX, minSpanY)); + } + continue; + } } else if (o instanceof ResolveInfo) { ResolveInfo resolveInfo = (ResolveInfo) o; componentName = new ComponentName(resolveInfo.activityInfo.packageName, @@ -139,7 +160,7 @@ public class WidgetsModel { if (widgetsShortcutsList != null) { widgetsShortcutsList.add(o); } else { - widgetsShortcutsList = new ArrayList<Object>(); + widgetsShortcutsList = new ArrayList<>(); widgetsShortcutsList.add(o); pInfo = new PackageItemInfo(packageName); mIconCache.getTitleAndIconForApp(packageName, userHandle, diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java index 34492e4ca..8702877bf 100644 --- a/src/com/android/launcher3/testing/LauncherExtension.java +++ b/src/com/android/launcher3/testing/LauncherExtension.java @@ -202,6 +202,11 @@ public class LauncherExtension extends Launcher { } @Override + public boolean startSearchFromAllApps(String query) { + return false; + } + + @Override public void startVoice() { } diff --git a/src/com/android/launcher3/util/ComponentKey.java b/src/com/android/launcher3/util/ComponentKey.java index 6a7df4318..b7aafaea9 100644 --- a/src/com/android/launcher3/util/ComponentKey.java +++ b/src/com/android/launcher3/util/ComponentKey.java @@ -64,8 +64,11 @@ public class ComponentKey { * 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); + String flattened = componentName.flattenToString(); + if (user != null) { + flattened += "#" + UserManagerCompat.getInstance(context).getSerialNumberForUser(user); + } + return flattened; } @Override diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java index b37f44719..74fc92a04 100644 --- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -68,7 +68,7 @@ public class ManagedProfileHeuristic { 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)) { + if (Utilities.ATLEAST_LOLLIPOP && !UserHandleCompat.myUserHandle().equals(user)) { return new ManagedProfileHeuristic(context, user); } return null; @@ -296,7 +296,7 @@ public class ManagedProfileHeuristic { * 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()) { + if (!Utilities.ATLEAST_LOLLIPOP) { return; } UserManagerCompat userManager = UserManagerCompat.getInstance(context); diff --git a/src/com/android/launcher3/util/UiThreadCircularReveal.java b/src/com/android/launcher3/util/UiThreadCircularReveal.java index 3ca3aeeee..f2b5e5e15 100644 --- a/src/com/android/launcher3/util/UiThreadCircularReveal.java +++ b/src/com/android/launcher3/util/UiThreadCircularReveal.java @@ -47,7 +47,7 @@ public class UiThreadCircularReveal { float progress = arg0.getAnimatedFraction(); outlineProvider.setProgress(progress); revealView.invalidateOutline(); - if (!Utilities.isLmpMR1OrAbove()) { + if (!Utilities.ATLEAST_LOLLIPOP_MR1) { revealView.invalidate(); } } diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java index 53b2acd84..b9fccbcfd 100644 --- a/src/com/android/launcher3/util/WallpaperUtils.java +++ b/src/com/android/launcher3/util/WallpaperUtils.java @@ -24,6 +24,8 @@ import android.graphics.Point; import android.os.Build; import android.view.WindowManager; +import com.android.launcher3.Utilities; + /** * Utility methods for wallpaper management. */ @@ -99,7 +101,7 @@ public final class WallpaperUtils { 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) { + if (Utilities.ATLEAST_JB_MR1) { Point realSize = new Point(); windowManager.getDefaultDisplay().getRealSize(realSize); maxDim = Math.max(realSize.x, realSize.y); diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index 758287af3..fcb714ff1 100644 --- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -31,10 +31,6 @@ import com.android.launcher3.compat.AppWidgetManagerCompat; * @see {@link PendingAddItemInfo} */ public class PendingAddWidgetInfo extends PendingAddItemInfo { - public int minWidth; - public int minHeight; - public int minResizeWidth; - public int minResizeHeight; public int previewImage; public int icon; public LauncherAppWidgetProviderInfo info; @@ -50,17 +46,13 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo { this.info = i; user = AppWidgetManagerCompat.getInstance(launcher).getUser(i); componentName = i.provider; - minWidth = i.minWidth; - minHeight = i.minHeight; - minResizeWidth = i.minResizeWidth; - minResizeHeight = i.minResizeHeight; previewImage = i.previewImage; icon = i.icon; - spanX = i.getSpanX(launcher); - spanY = i.getSpanY(launcher); - minSpanX = i.getMinSpanX(launcher); - minSpanY = i.getMinSpanY(launcher); + spanX = i.spanX; + spanY = i.spanY; + minSpanX = i.minSpanX; + minSpanY = i.minSpanY; } public boolean isCustomWidget() { diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index 7496ea2ef..94bbd929f 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -146,8 +146,8 @@ public class WidgetCell extends LinearLayout implements OnLayoutChangeListener { mInfo = info; // TODO(hyunyoungs): setup a cache for these labels. mWidgetName.setText(AppWidgetManagerCompat.getInstance(getContext()).loadLabel(info)); - int hSpan = Math.min(info.getSpanX(mLauncher), profile.numColumns); - int vSpan = Math.min(info.getSpanY(mLauncher), profile.numRows); + int hSpan = Math.min(info.spanX, profile.numColumns); + int vSpan = Math.min(info.spanY, profile.numRows); mWidgetDims.setText(String.format(mDimensionsFormatString, hSpan, vSpan)); mWidgetPreviewLoader = loader; } diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java index 30b3d581a..461aebb6b 100644 --- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -15,6 +15,7 @@ import com.android.launcher3.DragLayer; import com.android.launcher3.DragSource; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.util.Thunk; @@ -131,7 +132,7 @@ public class WidgetHostViewLoader implements DragListener { public static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) { Bundle options = null; Rect rect = new Rect(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, rect); Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher, info.componentName, null); diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java index 5afd7c493..0c6ea31bb 100644 --- a/src/com/android/launcher3/widget/WidgetsContainerView.java +++ b/src/com/android/launcher3/widget/WidgetsContainerView.java @@ -319,7 +319,6 @@ public class WidgetsContainerView extends BaseContainerView CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen); ItemInfo itemInfo = (ItemInfo) d.dragInfo; if (layout != null) { - layout.calculateSpans(itemInfo); showOutOfSpaceMessage = !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY); } diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java index d2ea25230..f1cde299d 100644 --- a/src/com/android/launcher3/widget/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -87,6 +87,9 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { @Override public int getItemCount() { + if (mWidgetsModel == null) { + return 0; + } return mWidgetsModel.getPackageSize(); } @@ -166,7 +169,7 @@ public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> { // if the end padding is 0, then container view (horizontal scroll view) doesn't respect // the end of the linear layout width + the start padding and doesn't allow scrolling. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Utilities.ATLEAST_JB_MR1) { cellList.setPaddingRelative(mIndent, 0, 1, 0); } else { cellList.setPadding(mIndent, 0, 1, 0); diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java index 61e63cdb7..884bdc418 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java @@ -64,10 +64,6 @@ public class WidgetsRecyclerView extends BaseRecyclerView { return Color.WHITE; } - public int getFastScrollerThumbInactiveColor(int defaultInactiveThumbColor) { - return getResources().getColor(R.color.widgets_view_fastscroll_thumb_inactive_color); - } - /** * Sets the widget model in this view, used to determine the fast scroll position. */ @@ -92,6 +88,12 @@ public class WidgetsRecyclerView extends BaseRecyclerView { */ @Override public String scrollToPositionAtProgress(float touchFraction) { + // Skip early if widgets are not bound. + if (mWidgets == null) { + return ""; + } + + // Skip early if there are no widgets. int rowCount = mWidgets.getPackageSize(); if (rowCount == 0) { return ""; @@ -102,7 +104,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView { getCurScrollState(mScrollPosState); float pos = rowCount * touchFraction; - int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight, 0); + int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight); LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); @@ -115,36 +117,44 @@ public class WidgetsRecyclerView extends BaseRecyclerView { * Updates the bounds for the scrollbar. */ @Override - public void onUpdateScrollbar() { - int rowCount = mWidgets.getPackageSize(); + public void onUpdateScrollbar(int dy) { + // Skip early if widgets are not bound. + if (mWidgets == null) { + return; + } - // Skip early if, there are no items. + // Skip early if there are no widgets. + int rowCount = mWidgets.getPackageSize(); if (rowCount == 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } // Skip early if, there no child laid out in the container. getCurScrollState(mScrollPosState); if (mScrollPosState.rowIndex < 0) { - mScrollbar.setScrollbarThumbOffset(-1, -1); + mScrollbar.setThumbOffset(-1, -1); return; } - synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount, 0); + synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount); } /** * Returns the current scroll state. */ - private void getCurScrollState(ScrollPositionState stateOut) { + protected void getCurScrollState(ScrollPositionState stateOut) { stateOut.rowIndex = -1; stateOut.rowTopOffset = -1; stateOut.rowHeight = -1; - int rowCount = mWidgets.getPackageSize(); + // Skip early if widgets are not bound. + if (mWidgets == null) { + return; + } // Return early if there are no items + int rowCount = mWidgets.getPackageSize(); if (rowCount == 0) { return; } |