diff options
Diffstat (limited to 'src/com/android')
100 files changed, 3156 insertions, 1478 deletions
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 52a83dcdf..bd1268651 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -95,7 +95,7 @@ public abstract class AbstractFloatingView extends LinearLayout { return null; } - protected static void closeOpenContainer(Launcher launcher, @FloatingViewType int type) { + public static void closeOpenContainer(Launcher launcher, @FloatingViewType int type) { AbstractFloatingView view = getOpenView(launcher, type); if (view != null) { view.close(true); diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java index 9cce9b188..5b42cad96 100644 --- a/src/com/android/launcher3/AllAppsList.java +++ b/src/com/android/launcher3/AllAppsList.java @@ -208,16 +208,6 @@ public class AllAppsList { } /** - * Query the launcher apps service for whether the supplied package has - * MAIN/LAUNCHER activities in the supplied package. - */ - static boolean packageHasActivities(Context context, String packageName, - UserHandle user) { - final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context); - return launcherApps.getActivityList(packageName, user).size() > 0; - } - - /** * Returns whether <em>apps</em> contains <em>component</em>. */ private static boolean findActivity(ArrayList<AppInfo> apps, ComponentName component, diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java index 8bf49c27e..0ddde73c5 100644 --- a/src/com/android/launcher3/AppInfo.java +++ b/src/com/android/launcher3/AppInfo.java @@ -21,14 +21,11 @@ import android.content.Context; import android.content.Intent; import android.content.pm.LauncherActivityInfo; import android.os.UserHandle; -import android.util.Log; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageManagerHelper; -import java.util.ArrayList; - /** * Represents an app in AllAppsView. */ @@ -44,7 +41,7 @@ public class AppInfo extends ItemInfoWithIcon { /** * {@see ShortcutInfo#isDisabled} */ - int isDisabled = ShortcutInfo.DEFAULT; + public int isDisabled = ShortcutInfo.DEFAULT; public AppInfo() { itemType = LauncherSettings.BaseLauncherColumns.ITEM_TYPE_SHORTCUT; @@ -83,7 +80,6 @@ public class AppInfo extends ItemInfoWithIcon { title = Utilities.trim(info.title); intent = new Intent(info.intent); isDisabled = info.isDisabled; - iconBitmap = info.iconBitmap; } @Override diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java index 7bc36921e..84a8bce6e 100644 --- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java +++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java @@ -83,7 +83,7 @@ public class AppWidgetsRestoredReceiver extends BroadcastReceiver { LauncherAppState app = LauncherAppState.getInstanceNoCreate(); if (app != null) { - app.reloadWorkspace(); + app.getModel().forceReload(); } asyncResult.finish(); } diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java index 9f4f1f9c4..e1a3ad0d6 100644 --- a/src/com/android/launcher3/BaseActivity.java +++ b/src/com/android/launcher3/BaseActivity.java @@ -21,9 +21,12 @@ import android.content.Context; import android.content.ContextWrapper; import android.view.View.AccessibilityDelegate; +import com.android.launcher3.logging.UserEventDispatcher; + public abstract class BaseActivity extends Activity { protected DeviceProfile mDeviceProfile; + protected UserEventDispatcher mUserEventDispatcher; public DeviceProfile getDeviceProfile() { return mDeviceProfile; @@ -33,6 +36,13 @@ public abstract class BaseActivity extends Activity { return null; } + public final UserEventDispatcher getUserEventDispatcher() { + if (mUserEventDispatcher == null) { + mUserEventDispatcher = UserEventDispatcher.get(this); + } + return mUserEventDispatcher; + } + public static BaseActivity fromContext(Context context) { if (context instanceof BaseActivity) { return (BaseActivity) context; diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java index ba7c3f809..5feb42ea8 100644 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java @@ -36,10 +36,6 @@ import com.android.launcher3.util.Themes; */ public class BaseRecyclerViewFastScrollBar { - public interface FastScrollFocusableView { - void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated); - } - private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH = new Property<BaseRecyclerViewFastScrollBar, Integer>(Integer.class, "width") { diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 1a41e08db..f9a6742d8 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -40,11 +40,12 @@ import com.android.launcher3.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.badge.BadgeInfo; import com.android.launcher3.badge.BadgeRenderer; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.folder.FolderIconPreviewVerifier; import com.android.launcher3.graphics.DrawableFactory; import com.android.launcher3.graphics.HolographicOutlineHelper; -import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.graphics.PreloadIconDrawable; import com.android.launcher3.model.PackageItemInfo; +import com.android.launcher3.popup.PopupContainerWithArrow; import java.text.NumberFormat; @@ -53,8 +54,7 @@ import java.text.NumberFormat; * because we want to make the bubble taller than the text and TextView's clip is * too aggressive. */ -public class BubbleTextView extends TextView - implements BaseRecyclerViewFastScrollBar.FastScrollFocusableView, ItemInfoUpdateReceiver { +public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver { // Dimensions in DP private static final float AMBIENT_SHADOW_RADIUS = 2.5f; @@ -67,6 +67,8 @@ public class BubbleTextView extends TextView private static final int DISPLAY_ALL_APPS = 1; private static final int DISPLAY_FOLDER = 2; + private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed}; + private final Launcher mLauncher; private Drawable mIcon; private final boolean mCenterVertically; @@ -232,12 +234,19 @@ public class BubbleTextView extends TextView } @Override - public void setPressed(boolean pressed) { - super.setPressed(pressed); - + public void refreshDrawableState() { if (!mIgnorePressedStateChange) { - updateIconState(); + super.refreshDrawableState(); + } + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mStayPressed) { + mergeDrawableStates(drawableState, STATE_PRESSED); } + return drawableState; } /** Returns the icon for this view. */ @@ -250,17 +259,6 @@ public class BubbleTextView extends TextView return mLayoutHorizontal; } - private void updateIconState() { - if (mIcon instanceof FastBitmapDrawable) { - FastBitmapDrawable d = (FastBitmapDrawable) mIcon; - if (isPressed() || mStayPressed) { - d.animateState(FastBitmapDrawable.State.PRESSED); - } else { - d.animateState(FastBitmapDrawable.State.NORMAL); - } - } - } - @Override public void setOnLongClickListener(OnLongClickListener l) { super.setOnLongClickListener(l); @@ -334,7 +332,7 @@ public class BubbleTextView extends TextView this, mPressedBackground); } - updateIconState(); + refreshDrawableState(); } void clearPressedBackground() { @@ -364,7 +362,7 @@ public class BubbleTextView extends TextView mPressedBackground = null; mIgnorePressedStateChange = false; - updateIconState(); + refreshDrawableState(); return result; } @@ -505,15 +503,14 @@ public class BubbleTextView extends TextView if (mIcon instanceof FastBitmapDrawable) { BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo); BadgeRenderer badgeRenderer = mLauncher.getDeviceProfile().mBadgeRenderer; + PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(mLauncher); + if (popup != null) { + popup.updateNotificationHeader(badgeInfo, itemInfo); + } ((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer, animate); } } - public IconPalette getIconPalette() { - return mIcon instanceof FastBitmapDrawable ? ((FastBitmapDrawable) mIcon).getIconPalette() - : null; - } - /** * Sets the icon for this view based on the layout direction. */ @@ -544,10 +541,6 @@ public class BubbleTextView extends TextView @Override public void reapplyItemInfo(ItemInfoWithIcon info) { if (getTag() == info) { - FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL; - if (mIcon instanceof FastBitmapDrawable) { - prevState = ((FastBitmapDrawable) mIcon).getCurrentState(); - } mIconLoadRequest = null; mDisableRelayout = true; @@ -555,7 +548,9 @@ public class BubbleTextView extends TextView applyFromApplicationInfo((AppInfo) info); } else if (info instanceof ShortcutInfo) { applyFromShortcutInfo((ShortcutInfo) info); - if ((info.rank < FolderIcon.NUM_ITEMS_IN_PREVIEW) && (info.container >= 0)) { + FolderIconPreviewVerifier verifier = + new FolderIconPreviewVerifier(mLauncher.getDeviceProfile().inv); + if (verifier.isItemInPreview(info.rank) && (info.container >= 0)) { View folderIcon = mLauncher.getWorkspace().getHomescreenIconByItemId(info.container); if (folderIcon != null) { @@ -566,12 +561,6 @@ public class BubbleTextView extends TextView applyFromPackageItemInfo((PackageItemInfo) info); } - // If we are reapplying over an old icon, then we should update the new icon to the same - // state as the old icon - if (mIcon instanceof FastBitmapDrawable) { - ((FastBitmapDrawable) mIcon).setState(prevState); - } - mDisableRelayout = false; } } @@ -593,34 +582,6 @@ public class BubbleTextView extends TextView } } - @Override - public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) { - // We can only set the fast scroll focus state on a FastBitmapDrawable - if (!(mIcon instanceof FastBitmapDrawable)) { - return; - } - - FastBitmapDrawable d = (FastBitmapDrawable) mIcon; - if (animated) { - FastBitmapDrawable.State prevState = d.getCurrentState(); - if (d.animateState(focusState)) { - // If the state was updated, then update the view accordingly - animate().scaleX(focusState.viewScale) - .scaleY(focusState.viewScale) - .setStartDelay(getStartDelayForStateChange(prevState, focusState)) - .setDuration(d.getDurationForStateChange(prevState, focusState)) - .start(); - } - } else { - if (d.setState(focusState)) { - // If the state was updated, then update the view accordingly - animate().cancel(); - setScaleX(focusState.viewScale); - setScaleY(focusState.viewScale); - } - } - } - /** * Returns true if the view can show custom shortcuts. */ @@ -629,19 +590,8 @@ public class BubbleTextView extends TextView .isEmpty(); } - /** - * Returns the start delay when animating between certain {@link FastBitmapDrawable} states. - */ - private static int getStartDelayForStateChange(final FastBitmapDrawable.State fromState, - final FastBitmapDrawable.State toState) { - switch (toState) { - case NORMAL: - switch (fromState) { - case FAST_SCROLL_HIGHLIGHTED: - return FastBitmapDrawable.FAST_SCROLL_INACTIVE_DURATION / 4; - } - } - return 0; + public int getIconSize() { + return mIconSize; } /** diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index 8d69fe374..8a477d809 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -42,6 +42,7 @@ import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.dragndrop.DragView; +import com.android.launcher3.util.Themes; import com.android.launcher3.util.Thunk; /** @@ -142,8 +143,8 @@ public abstract class ButtonDropTarget extends TextView mCurrentFilter = new ColorMatrix(); } - DragView.setColorScale(getTextColor(), mSrcFilter); - DragView.setColorScale(targetColor, mDstFilter); + Themes.setColorScaleOnMatrix(getTextColor(), mSrcFilter); + Themes.setColorScaleOnMatrix(targetColor, mDstFilter); ValueAnimator anim1 = ValueAnimator.ofObject( new FloatArrayEvaluator(mCurrentFilter.getArray()), mSrcFilter.getArray(), mDstFilter.getArray()); diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index cac6c065a..c0946a0e3 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -51,7 +51,7 @@ import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; import com.android.launcher3.accessibility.FolderAccessibilityHelper; import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper; import com.android.launcher3.anim.PropertyListBuilder; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.graphics.DragPreviewProvider; import com.android.launcher3.util.CellAndSpan; @@ -106,7 +106,6 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { private ArrayList<FolderIcon.PreviewBackground> mFolderBackgrounds = new ArrayList<FolderIcon.PreviewBackground>(); FolderIcon.PreviewBackground mFolderLeaveBehind = new FolderIcon.PreviewBackground(); - Paint mFolderBgPaint = new Paint(); private float mBackgroundAlpha; @@ -502,9 +501,9 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation); canvas.save(); canvas.translate(mTempLocation[0], mTempLocation[1]); - bg.drawBackground(canvas, mFolderBgPaint); + bg.drawBackground(canvas); if (!bg.isClipping) { - bg.drawBackgroundStroke(canvas, mFolderBgPaint); + bg.drawBackgroundStroke(canvas); } canvas.restore(); } @@ -514,7 +513,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { mFolderLeaveBehind.delegateCellY, mTempLocation); canvas.save(); canvas.translate(mTempLocation[0], mTempLocation[1]); - mFolderLeaveBehind.drawLeaveBehind(canvas, mFolderBgPaint); + mFolderLeaveBehind.drawLeaveBehind(canvas); canvas.restore(); } } @@ -529,7 +528,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation); canvas.save(); canvas.translate(mTempLocation[0], mTempLocation[1]); - bg.drawBackgroundStroke(canvas, mFolderBgPaint); + bg.drawBackgroundStroke(canvas); canvas.restore(); } } @@ -570,7 +569,7 @@ public class CellLayout extends ViewGroup implements BubbleTextShadowHandler { try { dispatchRestoreInstanceState(states); } catch (IllegalArgumentException ex) { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { throw ex; } // Mismatched viewId / viewType preventing restore. Skip restore on production builds. diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java deleted file mode 100644 index a43ab6723..000000000 --- a/src/com/android/launcher3/DeferredHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2008 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; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.MessageQueue; - -import com.android.launcher3.util.Thunk; - -import java.util.LinkedList; - -/** - * Queue of things to run on a looper thread. Items posted with {@link #post} will not - * be actually enqued on the handler until after the last one has run, to keep from - * starving the thread. - * - * This class is fifo. - */ -public class DeferredHandler { - @Thunk LinkedList<Runnable> mQueue = new LinkedList<>(); - private MessageQueue mMessageQueue = Looper.myQueue(); - private Impl mHandler = new Impl(); - - @Thunk class Impl extends Handler implements MessageQueue.IdleHandler { - public void handleMessage(Message msg) { - Runnable r; - synchronized (mQueue) { - if (mQueue.size() == 0) { - return; - } - r = mQueue.removeFirst(); - } - r.run(); - synchronized (mQueue) { - scheduleNextLocked(); - } - } - - public boolean queueIdle() { - handleMessage(null); - return false; - } - } - - private class IdleRunnable implements Runnable { - Runnable mRunnable; - - IdleRunnable(Runnable r) { - mRunnable = r; - } - - public void run() { - mRunnable.run(); - } - } - - public DeferredHandler() { - } - - /** Schedule runnable to run after everything that's on the queue right now. */ - public void post(Runnable runnable) { - synchronized (mQueue) { - mQueue.add(runnable); - if (mQueue.size() == 1) { - scheduleNextLocked(); - } - } - } - - /** Schedule runnable to run when the queue goes idle. */ - public void postIdle(final Runnable runnable) { - post(new IdleRunnable(runnable)); - } - - public void cancelAll() { - synchronized (mQueue) { - mQueue.clear(); - } - } - - /** Runs all queued Runnables from the calling thread. */ - public void flush() { - LinkedList<Runnable> queue = new LinkedList<>(); - synchronized (mQueue) { - queue.addAll(mQueue); - mQueue.clear(); - } - for (Runnable r : queue) { - r.run(); - } - } - - void scheduleNextLocked() { - if (mQueue.size() > 0) { - Runnable peek = mQueue.getFirst(); - if (peek instanceof IdleRunnable) { - mMessageQueue.addIdleHandler(mHandler); - } else { - mHandler.sendEmptyMessage(1); - } - } - } -} - diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index c9e3d4f10..43f7d2317 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -32,6 +32,7 @@ import android.widget.FrameLayout; import com.android.launcher3.CellLayout.ContainerType; import com.android.launcher3.badge.BadgeRenderer; +import com.android.launcher3.config.FeatureFlags; import java.util.ArrayList; @@ -530,10 +531,13 @@ public class DeviceProfile { workspacePadding.bottom); workspace.setPageSpacing(getWorkspacePageSpacing()); - View qsbContainer = launcher.getQsbContainer(); - lp = (FrameLayout.LayoutParams) qsbContainer.getLayoutParams(); - lp.topMargin = mInsets.top + workspacePadding.top; - qsbContainer.setLayoutParams(lp); + // Only display when enabled + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + View qsbContainer = launcher.getQsbContainer(); + lp = (FrameLayout.LayoutParams) qsbContainer.getLayoutParams(); + lp.topMargin = mInsets.top + workspacePadding.top; + qsbContainer.setLayoutParams(lp); + } // Layout the hotseat Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat); diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java index d05673ccc..596aa8f7b 100644 --- a/src/com/android/launcher3/ExtendedEditText.java +++ b/src/com/android/launcher3/ExtendedEditText.java @@ -29,6 +29,7 @@ import android.widget.EditText; public class ExtendedEditText extends EditText { private boolean mShowImeAfterFirstLayout; + private boolean mForceDisableSuggestions = false; /** * Implemented by listeners of the back key. @@ -107,4 +108,17 @@ public class ExtendedEditText extends EditText { mBackKeyListener.onBackKey(); } } + + /** + * Set to true when you want isSuggestionsEnabled to return false. + * Use this to disable the red underlines that appear under typos when suggestions is enabled. + */ + public void forceDisableSuggestions(boolean forceDisableSuggestions) { + mForceDisableSuggestions = forceDisableSuggestions; + } + + @Override + public boolean isSuggestionsEnabled() { + return !mForceDisableSuggestions && super.isSuggestionsEnabled(); + } } diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java index 1f74c8877..5a44f7504 100644 --- a/src/com/android/launcher3/FastBitmapDrawable.java +++ b/src/com/android/launcher3/FastBitmapDrawable.java @@ -16,7 +16,6 @@ package com.android.launcher3; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Bitmap; @@ -32,42 +31,19 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.util.Property; import android.util.SparseArray; -import android.view.animation.DecelerateInterpolator; import com.android.launcher3.badge.BadgeInfo; import com.android.launcher3.badge.BadgeRenderer; import com.android.launcher3.graphics.IconPalette; public class FastBitmapDrawable extends Drawable { + + private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed}; + + private static final float PRESSED_BRIGHTNESS = 100f / 255f; private static final float DISABLED_DESATURATION = 1f; private static final float DISABLED_BRIGHTNESS = 0.5f; - /** - * The possible states that a FastBitmapDrawable can be in. - */ - public enum State { - - NORMAL (0f, 0f, 1f, new DecelerateInterpolator()), - PRESSED (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR), - FAST_SCROLL_HIGHLIGHTED (0f, 0f, 1.15f, new DecelerateInterpolator()), - FAST_SCROLL_UNHIGHLIGHTED (0f, 0f, 1f, new DecelerateInterpolator()); - - public final float desaturation; - public final float brightness; - /** - * Used specifically by the view drawing this FastBitmapDrawable. - */ - public final float viewScale; - public final TimeInterpolator interpolator; - - State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) { - this.desaturation = desaturation; - this.brightness = brightness; - this.viewScale = viewScale; - this.interpolator = interpolator; - } - } - public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @Override @@ -82,10 +58,6 @@ public class FastBitmapDrawable extends Drawable { } }; public static final int CLICK_FEEDBACK_DURATION = 2000; - public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225; - public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150; - public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225; - public static final int FAST_SCROLL_INACTIVE_DURATION = 275; // Since we don't need 256^2 values for combinations of both the brightness and saturation, we // reduce the value space to a smaller value V, which reduces the number of cached @@ -101,7 +73,8 @@ public class FastBitmapDrawable extends Drawable { protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); private final Bitmap mBitmap; - private State mState = State.NORMAL; + + private boolean mIsPressed; private boolean mIsDisabled; private BadgeInfo mBadgeInfo; @@ -123,6 +96,19 @@ public class FastBitmapDrawable extends Drawable { } }; + private static final Property<FastBitmapDrawable, Float> BRIGHTNESS + = new Property<FastBitmapDrawable, Float>(Float.TYPE, "brightness") { + @Override + public Float get(FastBitmapDrawable fastBitmapDrawable) { + return fastBitmapDrawable.getBrightness(); + } + + @Override + public void set(FastBitmapDrawable fastBitmapDrawable, Float value) { + fastBitmapDrawable.setBrightness(value); + } + }; + // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and // as a result, can be used to compose the key for the cached ColorMatrixColorFilters private int mDesaturation = 0; @@ -130,8 +116,8 @@ public class FastBitmapDrawable extends Drawable { private int mAlpha = 255; private int mPrevUpdateKey = Integer.MAX_VALUE; - // Animators for the fast bitmap drawable's properties - private AnimatorSet mPropertyAnimator; + // Animators for the fast bitmap drawable's brightness + private ObjectAnimator mBrightnessAnimator; public FastBitmapDrawable(Bitmap b) { mBitmap = b; @@ -181,7 +167,7 @@ public class FastBitmapDrawable extends Drawable { } } - public IconPalette getIconPalette() { + protected IconPalette getIconPalette() { if (mIconPalette == null) { mIconPalette = IconPalette.fromDominantColor(Utilities .findDominantColorByHue(mBitmap, 20)); @@ -243,60 +229,50 @@ public class FastBitmapDrawable extends Drawable { return mBitmap; } - /** - * Animates this drawable to a new state. - * - * @return whether the state has changed. - */ - public boolean animateState(State newState) { - State prevState = mState; - if (mState != newState) { - mState = newState; - - float desaturation = mIsDisabled ? DISABLED_DESATURATION : newState.desaturation; - float brightness = mIsDisabled ? DISABLED_BRIGHTNESS: newState.brightness; - - mPropertyAnimator = cancelAnimator(mPropertyAnimator); - mPropertyAnimator = new AnimatorSet(); - mPropertyAnimator.playTogether( - ObjectAnimator.ofFloat(this, "desaturation", desaturation), - ObjectAnimator.ofFloat(this, "brightness", brightness)); - mPropertyAnimator.setInterpolator(newState.interpolator); - mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState)); - mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState)); - mPropertyAnimator.start(); - return true; - } - return false; + @Override + public boolean isStateful() { + return true; } - /** - * Immediately sets this drawable to a new state. - * - * @return whether the state has changed. - */ - public boolean setState(State newState) { - if (mState != newState) { - mState = newState; + @Override + protected boolean onStateChange(int[] state) { + boolean isPressed = false; + for (int s : state) { + if (s == android.R.attr.state_pressed) { + isPressed = true; + break; + } + } + if (mIsPressed != isPressed) { + mIsPressed = isPressed; - mPropertyAnimator = cancelAnimator(mPropertyAnimator); + if (mBrightnessAnimator != null) { + mBrightnessAnimator.cancel(); + } - invalidateDesaturationAndBrightness(); + if (mIsPressed) { + // Animate when going to pressed state + mBrightnessAnimator = ObjectAnimator.ofFloat( + this, BRIGHTNESS, getExpectedBrightness()); + mBrightnessAnimator.setDuration(CLICK_FEEDBACK_DURATION); + mBrightnessAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR); + mBrightnessAnimator.start(); + } else { + setBrightness(getExpectedBrightness()); + } return true; } return false; } private void invalidateDesaturationAndBrightness() { - setDesaturation(mIsDisabled ? DISABLED_DESATURATION : mState.desaturation); - setBrightness(mIsDisabled ? DISABLED_BRIGHTNESS: mState.brightness); + setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0); + setBrightness(getExpectedBrightness()); } - /** - * Returns the current state. - */ - public State getCurrentState() { - return mState; + private float getExpectedBrightness() { + return mIsDisabled ? DISABLED_BRIGHTNESS : + (mIsPressed ? PRESSED_BRIGHTNESS : 0); } public void setIsDisabled(boolean isDisabled) { @@ -307,49 +283,6 @@ public class FastBitmapDrawable extends Drawable { } /** - * Returns the duration for the state change animation. - */ - public static int getDurationForStateChange(State fromState, State toState) { - switch (toState) { - case NORMAL: - switch (fromState) { - case PRESSED: - return 0; - case FAST_SCROLL_HIGHLIGHTED: - case FAST_SCROLL_UNHIGHLIGHTED: - return FAST_SCROLL_INACTIVE_DURATION; - } - case PRESSED: - return CLICK_FEEDBACK_DURATION; - case FAST_SCROLL_HIGHLIGHTED: - return FAST_SCROLL_HIGHLIGHT_DURATION; - case FAST_SCROLL_UNHIGHLIGHTED: - switch (fromState) { - case NORMAL: - // When animating from normal state, take a little longer - return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION; - default: - return FAST_SCROLL_UNHIGHLIGHT_DURATION; - } - } - return 0; - } - - /** - * Returns the start delay when animating between certain fast scroll states. - */ - public static int getStartDelayForStateChange(State fromState, State toState) { - switch (toState) { - case FAST_SCROLL_UNHIGHLIGHTED: - switch (fromState) { - case NORMAL: - return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4; - } - } - return 0; - } - - /** * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] */ private void setDesaturation(float desaturation) { @@ -435,11 +368,4 @@ public class FastBitmapDrawable extends Drawable { } invalidateSelf(); } - - private AnimatorSet cancelAnimator(AnimatorSet animator) { - if (animator != null) { - animator.cancel(); - } - return null; - } } diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java index b36734bab..fe7acda17 100644 --- a/src/com/android/launcher3/FocusHelper.java +++ b/src/com/android/launcher3/FocusHelper.java @@ -22,7 +22,7 @@ import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewGroup; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderPagedView; import com.android.launcher3.util.FocusLogic; @@ -93,7 +93,7 @@ public class FocusHelper { } if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { throw new IllegalStateException("Parent of the focused item is not supported."); } else { return false; diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java index 2c69d0a69..0041bb4d6 100644 --- a/src/com/android/launcher3/FolderInfo.java +++ b/src/com/android/launcher3/FolderInfo.java @@ -114,11 +114,18 @@ public class FolderInfo extends ItemInfo { } } + public void prepareAutoUpdate() { + for (int i = 0; i < listeners.size(); i++) { + listeners.get(i).prepareAutoUpdate(); + } + } + public interface FolderListener { public void onAdd(ShortcutInfo item); public void onRemove(ShortcutInfo item); public void onTitleChanged(CharSequence title); public void onItemsChanged(boolean animate); + public void prepareAutoUpdate(); } public boolean hasOption(int optionFlag) { diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java index 1bab77449..80dec1670 100644 --- a/src/com/android/launcher3/InstallShortcutReceiver.java +++ b/src/com/android/launcher3/InstallShortcutReceiver.java @@ -218,6 +218,10 @@ public class InstallShortcutReceiver extends BroadcastReceiver { queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, widgetId, context), context); } + public static void queueActivityInfo(LauncherActivityInfo activity, Context context) { + queuePendingShortcutInfo(new PendingInstallShortcutInfo(activity, context), context); + } + public static HashSet<ShortcutKey> getPendingShortcuts(Context context) { HashSet<ShortcutKey> result = new HashSet<>(); diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 8aeab8712..146c2eee7 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -28,7 +28,6 @@ import android.view.Display; import android.view.WindowManager; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.logging.FileLog; import com.android.launcher3.util.Thunk; @@ -335,7 +334,7 @@ public class InvariantDeviceProfile { } public int getAllAppsButtonRank() { - if (ProviderConfig.IS_DOGFOOD_BUILD && FeatureFlags.NO_ALL_APPS_ICON) { + if (FeatureFlags.IS_DOGFOOD_BUILD && FeatureFlags.NO_ALL_APPS_ICON) { throw new IllegalAccessError("Accessing all apps rank when all-apps is disabled"); } return numHotseatIcons / 2; diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java index a3d8c6a9d..1e020e258 100644 --- a/src/com/android/launcher3/ItemInfoWithIcon.java +++ b/src/com/android/launcher3/ItemInfoWithIcon.java @@ -35,7 +35,9 @@ public abstract class ItemInfoWithIcon extends ItemInfo { protected ItemInfoWithIcon() { } - protected ItemInfoWithIcon(ItemInfo info) { + protected ItemInfoWithIcon(ItemInfoWithIcon info) { super(info); + iconBitmap = info.iconBitmap; + usingLowResIcon = info.usingLowResIcon; } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index c5cefa678..a69f501d7 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -72,6 +72,7 @@ import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.OvershootInterpolator; @@ -86,15 +87,11 @@ import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.allapps.DefaultAppSearchController; import com.android.launcher3.anim.AnimationLayerSet; -import com.android.launcher3.model.ModelWriter; -import com.android.launcher3.notification.NotificationListener; -import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PinItemRequestCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; @@ -107,10 +104,13 @@ import com.android.launcher3.keyboard.CustomActionsPopup; import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.logging.FileLog; import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.PackageItemInfo; import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pageindicators.PageIndicator; import com.android.launcher3.popup.PopupContainerWithArrow; +import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.userevent.nano.LauncherLogProto; @@ -120,7 +120,6 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; import com.android.launcher3.util.ActivityResultInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.LogConfig; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; @@ -176,6 +175,13 @@ public class Launcher extends BaseActivity */ protected static final int REQUEST_LAST = 100; + private static final int SOFT_INPUT_MODE_DEFAULT = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; + private static final int SOFT_INPUT_MODE_ALL_APPS = + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; + // The Intent extra that defines whether to ignore the launch animation static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION = "com.android.launcher3.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION"; @@ -316,8 +322,6 @@ public class Launcher extends BaseActivity */ private PendingRequestArgs mPendingRequestArgs; - private UserEventDispatcher mUserEventDispatcher; - private float mLastDispatchTouchEventX = 0.0f; public ViewGroupFocusHelper mFocusHandler; @@ -628,23 +632,6 @@ public class Launcher extends BaseActivity } } - public UserEventDispatcher getUserEventDispatcher() { - if (mLauncherCallbacks != null) { - UserEventDispatcher dispatcher = mLauncherCallbacks.getUserEventDispatcher(); - if (dispatcher != null) { - return dispatcher; - } - } - - // Logger object is a singleton and does not have to be coupled with the foreground - // activity. Since most user event logging is done on the UI, the object is retrieved - // from the callback for convenience. - if (mUserEventDispatcher == null) { - mUserEventDispatcher = new UserEventDispatcher(); - } - return mUserEventDispatcher; - } - 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(). @@ -1072,6 +1059,7 @@ public class Launcher extends BaseActivity if (mLauncherCallbacks != null) { mLauncherCallbacks.onResume(); } + } @Override @@ -1164,9 +1152,10 @@ public class Launcher extends BaseActivity if (mLauncherCallbacks != null) { return mLauncherCallbacks.hasSettings(); } else { - // On devices with a locked orientation, we will at least have the allow rotation - // setting. - return !getResources().getBoolean(R.bool.allow_rotation); + // On O and above we there is always some setting present settings (add icon to + // home screen or icon badging). On earlier APIs we will have the allow rotation + // setting, on devices with a locked orientation, + return Utilities.isAtLeastO() || !getResources().getBoolean(R.bool.allow_rotation); } } @@ -2472,7 +2461,7 @@ public class Launcher extends BaseActivity throw new IllegalArgumentException("Input must have a valid intent"); } boolean success = startActivitySafely(v, intent, item); - getUserEventDispatcher().logAppLaunch(v, intent); + getUserEventDispatcher().logAppLaunch(v, intent); // TODO for discovered apps b/35802115 if (success && v instanceof BubbleTextView) { mWaitingForResume = (BubbleTextView) v; @@ -2721,9 +2710,10 @@ public class Launcher extends BaseActivity intent.setSourceBounds(getViewBounds(v)); } try { - if (Utilities.ATLEAST_MARSHMALLOW && item != null + if (Utilities.ATLEAST_MARSHMALLOW + && (item instanceof ShortcutInfo) && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT - || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) + || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) && !((ShortcutInfo) item).isPromise()) { // Shortcuts need some special checks due to legacy reasons. startShortcutIntentSafely(intent, optsBundle, item); @@ -2896,7 +2886,7 @@ public class Launcher extends BaseActivity } // Change the state *after* we've called all the transition code - mState = State.WORKSPACE; + setState(State.WORKSPACE); if (changed) { // Send an accessibility event to announce the context change @@ -2933,12 +2923,30 @@ public class Launcher extends BaseActivity mWorkspace.setVisibility(View.VISIBLE); mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), Workspace.State.OVERVIEW, animated, postAnimRunnable); - mState = State.WORKSPACE; + setState(State.WORKSPACE); + // If animated from long press, then don't allow any of the controller in the drag // layer to intercept any remaining touch. mWorkspace.requestDisallowInterceptTouchEvent(animated); } + private void setState(State state) { + this.mState = state; + updateSoftInputMode(); + } + + private void updateSoftInputMode() { + if (FeatureFlags.LAUNCHER3_UPDATE_SOFT_INPUT_MODE) { + final int mode; + if (isAppsViewVisible()) { + mode = SOFT_INPUT_MODE_ALL_APPS; + } else { + mode = SOFT_INPUT_MODE_DEFAULT; + } + getWindow().setSoftInputMode(mode); + } + } + /** * Shows the apps view. */ @@ -3000,7 +3008,7 @@ public class Launcher extends BaseActivity } // Change the state *after* we've called all the transition code - mState = toState; + setState(toState); AbstractFloatingView.closeAllOpenViews(this); // Send an accessibility event to announce the context change @@ -3030,7 +3038,7 @@ public class Launcher extends BaseActivity mStateTransitionAnimation.startAnimationToWorkspace(mState, mWorkspace.getState(), Workspace.State.SPRING_LOADED, true /* animated */, null /* onCompleteRunnable */); - mState = State.WORKSPACE_SPRING_LOADED; + setState(State.WORKSPACE_SPRING_LOADED); } public void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay, @@ -3210,6 +3218,9 @@ public class Launcher extends BaseActivity if (LauncherAppState.PROFILE_STARTUP) { Trace.beginSection("Starting page bind"); } + + AbstractFloatingView.closeAllOpenViews(this); + setWorkspaceLoading(true); // Clear the workspace because it's going to be rebound @@ -3376,7 +3387,7 @@ public class Launcher extends BaseActivity Object tag = v.getTag(); String desc = "Collision while binding workspace item: " + item + ". Collides with " + tag; - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { throw (new RuntimeException(desc)); } else { Log.d(TAG, desc); diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index 43cf827da..aa7f5ee5f 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -130,17 +130,4 @@ public class LauncherAnimUtils { return anim; } - public static ValueAnimator animateViewHeight(final View v, int fromHeight, int toHeight) { - ValueAnimator anim = ValueAnimator.ofInt(fromHeight, toHeight); - anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - int val = (Integer) valueAnimator.getAnimatedValue(); - ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); - layoutParams.height = val; - v.setLayoutParams(layoutParams); - } - }); - return anim; - } } diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index e0e53a647..f2d66fe81 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -26,7 +26,7 @@ import android.util.Log; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dynamicui.ExtractionUtils; import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.util.ConfigMonitor; @@ -38,7 +38,7 @@ import java.util.concurrent.ExecutionException; public class LauncherAppState { - public static final boolean PROFILE_STARTUP = ProviderConfig.IS_DOGFOOD_BUILD; + public static final boolean PROFILE_STARTUP = FeatureFlags.IS_DOGFOOD_BUILD; // We do not need any synchronization for this variable as its only written on UI thread. private static LauncherAppState INSTANCE; @@ -134,15 +134,6 @@ public class LauncherAppState { PackageInstallerCompat.getInstance(mContext).onStop(); } - /** - * Reloads the workspace items from the DB and re-binds the workspace. This should generally - * not be called as DB updates are automatically followed by UI update - */ - public void reloadWorkspace() { - mModel.resetLoadedState(false, true); - mModel.startLoaderFromBackground(); - } - LauncherModel setLauncher(Launcher launcher) { getLocalProvider(mContext).setLauncherProviderChangeListener(launcher); mModel.initialize(launcher); diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java index 6394b9052..2bac11f97 100644 --- a/src/com/android/launcher3/LauncherCallbacks.java +++ b/src/com/android/launcher3/LauncherCallbacks.java @@ -92,7 +92,6 @@ public interface LauncherCallbacks { /* * Extensions points for adding / replacing some other aspects of the Launcher experience. */ - public UserEventDispatcher getUserEventDispatcher(); public boolean shouldMoveToDefaultScreenOnHomeIntent(); public boolean hasSettings(); public AllAppsSearchBarController getAllAppsSearchBarController(); diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 34d576d27..35811d38a 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -26,7 +26,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherActivityInfo; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; @@ -45,10 +44,11 @@ import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dynamicui.ExtractionUtils; import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.folder.FolderIconPreviewVerifier; import com.android.launcher3.graphics.LauncherIcons; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.AddWorkspaceItemsTask; @@ -72,8 +72,7 @@ import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.ContentWriter; -import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.ManagedProfileHeuristic; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageManagerHelper; @@ -85,7 +84,6 @@ import com.android.launcher3.util.ViewOnDrawExecutor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -95,6 +93,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; /** @@ -112,9 +111,9 @@ public class LauncherModel extends BroadcastReceiver private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private static final long INVALID_SCREEN_ID = -1L; + private final MainThreadExecutor mUiExecutor = new MainThreadExecutor(); @Thunk final LauncherAppState mApp; @Thunk final Object mLock = new Object(); - @Thunk DeferredHandler mHandler = new DeferredHandler(); @Thunk LoaderTask mLoaderTask; @Thunk boolean mIsLoaderTaskRunning; @Thunk boolean mHasLoaderCompletedOnce; @@ -125,12 +124,16 @@ public class LauncherModel extends BroadcastReceiver } @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper()); - // We start off with everything not loaded. After that, we assume that + // Indicates whether the current model data is valid or not. + // We start off with everything not loaded. After that, we assume that // our monitoring of the package manager provides all updates and we never - // need to do a requery. These are only ever touched from the loader thread. - private boolean mWorkspaceLoaded; - private boolean mAllAppsLoaded; - private boolean mDeepShortcutsLoaded; + // need to do a requery. This is only ever touched from the loader thread. + private boolean mModelLoaded; + public boolean isModelLoaded() { + synchronized (mLock) { + return mModelLoaded && mLoaderTask == null; + } + } /** * Set of runnables to be called on the background thread after the workspace binding @@ -150,11 +153,11 @@ public class LauncherModel extends BroadcastReceiver private final Runnable mShortcutPermissionCheckRunnable = new Runnable() { @Override public void run() { - if (mDeepShortcutsLoaded) { + if (mModelLoaded) { boolean hasShortcutHostPermission = DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission(); if (hasShortcutHostPermission != mHasShortcutHostPermission) { - mApp.reloadWorkspace(); + forceReload(); } } } @@ -216,17 +219,6 @@ public class LauncherModel extends BroadcastReceiver mUserManager = UserManagerCompat.getInstance(context); } - /** Runs the specified runnable immediately if called from the main thread, otherwise it is - * posted on the main thread handler. */ - private void runOnMainThread(Runnable r) { - if (sWorkerThread.getThreadId() == Process.myTid()) { - // If we are on the worker thread, post onto the main handler - mHandler.post(r); - } else { - r.run(); - } - } - /** 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) { @@ -376,8 +368,6 @@ public class LauncherModel extends BroadcastReceiver public void initialize(Callbacks callbacks) { synchronized (mLock) { Preconditions.assertUIThread(); - // Remove any queued UI runnables - mHandler.cancelAll(); mCallbacks = new WeakReference<>(callbacks); } } @@ -482,8 +472,16 @@ public class LauncherModel extends BroadcastReceiver } } - void forceReload() { - resetLoadedState(true, true); + /** + * Reloads the workspace items from the DB and re-binds the workspace. This should generally + * not be called as DB updates are automatically followed by UI update + */ + public void forceReload() { + synchronized (mLock) { + // Stop any existing loaders first, so they don't set mModelLoaded to true later + stopLoaderLocked(); + mModelLoaded = false; + } // Do this here because if the launcher activity is running it will be restarted. // If it's not running startLoaderFromBackground will merely tell it that it needs @@ -491,19 +489,6 @@ public class LauncherModel extends BroadcastReceiver startLoaderFromBackground(); } - public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) { - synchronized (mLock) { - // Stop any existing loaders first, so they don't set mAllAppsLoaded or - // mWorkspaceLoaded to true later - stopLoaderLocked(); - if (resetAllAppsLoaded) mAllAppsLoaded = false; - if (resetWorkspaceLoaded) mWorkspaceLoaded = false; - // Always reset deep shortcuts loaded. - // TODO: why? - mDeepShortcutsLoaded = false; - } - } - /** * When the launcher is in the background, it's possible for it to miss paired * configuration changes. So whenever we trigger the loader from the background @@ -546,18 +531,17 @@ public class LauncherModel extends BroadcastReceiver if (mCallbacks != null && mCallbacks.get() != null) { final Callbacks oldCallbacks = mCallbacks.get(); // Clear any pending bind-runnables from the synchronized load process. - runOnMainThread(new Runnable() { - public void run() { - oldCallbacks.clearPendingBinds(); - } - }); + mUiExecutor.execute(new Runnable() { + public void run() { + oldCallbacks.clearPendingBinds(); + } + }); // If there is already one running, tell it to stop. stopLoaderLocked(); mLoaderTask = new LoaderTask(mApp.getContext(), synchronousBindPage); - // TODO: mDeepShortcutsLoaded does not need to be true for synchronous bind. - if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE && mAllAppsLoaded - && mWorkspaceLoaded && mDeepShortcutsLoaded && !mIsLoaderTaskRunning) { + if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE + && mModelLoaded && !mIsLoaderTaskRunning) { mLoaderTask.runBindSynchronousPage(synchronousBindPage); return true; } else { @@ -602,68 +586,21 @@ public class LauncherModel extends BroadcastReceiver @Thunk boolean mIsLoadingAndBindingWorkspace; private boolean mStopped; - @Thunk boolean mLoadAndBindStepFinished; LoaderTask(Context context, int pageToBindFirst) { mContext = context; mPageToBindFirst = pageToBindFirst; } - private void loadAndBindWorkspace() { - mIsLoadingAndBindingWorkspace = true; - - // Load the workspace - if (DEBUG_LOADERS) { - Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); - } - - if (!mWorkspaceLoaded) { - loadWorkspace(); - synchronized (LoaderTask.this) { - if (mStopped) { - return; - } - mWorkspaceLoaded = true; - } - } - - // Bind the workspace - bindWorkspace(mPageToBindFirst); - } - private void waitForIdle() { // Wait until the either we're stopped or the other threads are done. // This way we don't start loading all apps until the workspace has settled // down. synchronized (LoaderTask.this) { - final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; - - mHandler.postIdle(new Runnable() { - public void run() { - synchronized (LoaderTask.this) { - mLoadAndBindStepFinished = true; - if (DEBUG_LOADERS) { - Log.d(TAG, "done with previous binding step"); - } - LoaderTask.this.notify(); - } - } - }); - - while (!mStopped && !mLoadAndBindStepFinished) { - try { - // Just in case mFlushingWorkerThread changes but we aren't woken up, - // wait no longer than 1sec at a time - this.wait(1000); - } catch (InterruptedException ex) { - // Ignore - } - } - if (DEBUG_LOADERS) { - Log.d(TAG, "waited " - + (SystemClock.uptimeMillis()-workspaceWaitTime) - + "ms for previous step to finish binding"); - } + LooperIdleLock idleLock = new LooperIdleLock(this, Looper.getMainLooper()); + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + while (!mStopped && idleLock.awaitLocked(1000)); } } @@ -673,7 +610,7 @@ public class LauncherModel extends BroadcastReceiver throw new RuntimeException("Should not call runBindSynchronousPage() without " + "valid page index"); } - if (!mAllAppsLoaded || !mWorkspaceLoaded) { + if (!mModelLoaded) { // Ensure that we don't try and bind a specified page when the pages have not been // loaded already (we should load everything asynchronously in that case) throw new RuntimeException("Expecting AllApps and Workspace to be loaded"); @@ -686,15 +623,6 @@ public class LauncherModel extends BroadcastReceiver } } - // XXX: Throw an exception if we are already loading (since we touch the worker thread - // data structures, we can't allow any other thread to touch that data, but because - // this call is synchronous, we can get away with not locking). - - // The LauncherModel is static in the LauncherAppState and mHandler may have queued - // operations from the previous activity. We need to ensure that all queued operations - // are executed before any synchronous binding work is done. - mHandler.flush(); - // Divide the set of loaded items into those that we are binding synchronously, and // everything else that is to be bound normally (asynchronously). bindWorkspace(synchronousBindPage); @@ -705,6 +633,14 @@ public class LauncherModel extends BroadcastReceiver bindDeepShortcuts(); } + private void verifyNotStopped() throws CancellationException { + synchronized (LoaderTask.this) { + if (mStopped) { + throw new CancellationException("Loader stopped"); + } + } + } + public void run() { synchronized (mLock) { if (mStopped) { @@ -712,41 +648,71 @@ public class LauncherModel extends BroadcastReceiver } mIsLoaderTaskRunning = true; } - // Optimize for end-user experience: if the Launcher is up and // running with the - // All Apps interface in the foreground, load All Apps first. Otherwise, load the - // workspace first (default). - keep_running: { - if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); - loadAndBindWorkspace(); - if (mStopped) { - break keep_running; - } + try { + long now = 0; + if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace"); + // Set to false in bindWorkspace() + mIsLoadingAndBindingWorkspace = true; + loadWorkspace(); + + verifyNotStopped(); + if (DEBUG_LOADERS) Log.d(TAG, "step 1.2: bind workspace workspace"); + bindWorkspace(mPageToBindFirst); + // Take a break + if (DEBUG_LOADERS) { + Log.d(TAG, "step 1 completed, wait for idle"); + now = SystemClock.uptimeMillis(); + } waitForIdle(); + if (DEBUG_LOADERS) Log.d(TAG, "Waited " + (SystemClock.uptimeMillis() - now) + "ms"); + verifyNotStopped(); // second step - if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); - loadAndBindAllApps(); + if (DEBUG_LOADERS) Log.d(TAG, "step 2.1: loading all apps"); + loadAllApps(); + + verifyNotStopped(); + if (DEBUG_LOADERS) Log.d(TAG, "step 2.2: Update icon cache"); + updateIconCache(); + // Take a break + if (DEBUG_LOADERS) { + Log.d(TAG, "step 2 completed, wait for idle"); + now = SystemClock.uptimeMillis(); + } waitForIdle(); + if (DEBUG_LOADERS) Log.d(TAG, "Waited " + (SystemClock.uptimeMillis() - now) + "ms"); + verifyNotStopped(); // third step - if (DEBUG_LOADERS) Log.d(TAG, "step 3: loading deep shortcuts"); - loadAndBindDeepShortcuts(); - } + if (DEBUG_LOADERS) Log.d(TAG, "step 3.1: loading deep shortcuts"); + loadDeepShortcuts(); - // Clear out this reference, otherwise we end up holding it until all of the - // callback runnables are done. - mContext = null; + verifyNotStopped(); + if (DEBUG_LOADERS) Log.d(TAG, "step 3.2: bind deep shortcuts"); + bindDeepShortcuts(); - synchronized (mLock) { - // If we are still the last one to be scheduled, remove ourselves. - if (mLoaderTask == this) { - mLoaderTask = null; + synchronized (mLock) { + // Everything loaded bind the data. + mModelLoaded = true; + mHasLoaderCompletedOnce = true; + } + } catch (CancellationException e) { + // Loader stopped, ignore + } finally { + // Clear out this reference, otherwise we end up holding it until all of the + // callback runnables are done. + mContext = null; + + synchronized (mLock) { + // If we are still the last one to be scheduled, remove ourselves. + if (mLoaderTask == this) { + mLoaderTask = null; + } + mIsLoaderTaskRunning = false; } - mIsLoaderTaskRunning = false; - mHasLoaderCompletedOnce = true; } } @@ -818,7 +784,7 @@ public class LauncherModel extends BroadcastReceiver if (clearDb) { Log.d(TAG, "loadWorkspace: resetting launcher database"); LauncherSettings.Settings.call(contentResolver, - LauncherSettings.Settings.METHOD_DELETE_DB); + LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); } Log.d(TAG, "loadWorkspace: loading default favorites"); @@ -886,6 +852,8 @@ public class LauncherModel extends BroadcastReceiver Intent intent; String targetPkg; + FolderIconPreviewVerifier verifier = + new FolderIconPreviewVerifier(mApp.getInvariantDeviceProfile()); while (!mStopped && c.moveToNext()) { try { if (c.user == null) { @@ -984,7 +952,7 @@ public class LauncherModel extends BroadcastReceiver c.markDeleted("Unrestored app removed: " + targetPkg); continue; } - } else if (pmHelper.isAppOnSdcard(targetPkg)) { + } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { // Package is present but not available. disabledState |= ShortcutInfo.FLAG_DISABLED_NOT_AVAILABLE; // Add the icon on the workspace anyway. @@ -1010,7 +978,7 @@ public class LauncherModel extends BroadcastReceiver } boolean useLowResIcon = !c.isOnWorkspaceOrHotseat() && - c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW; + !verifier.isItemInPreview(c.getInt(rankIndex)); if (c.restoreFlag != 0) { // Already verified above that user is same as default user @@ -1034,6 +1002,10 @@ public class LauncherModel extends BroadcastReceiver info = new ShortcutInfo(pinnedShortcut, context); info.iconBitmap = LauncherIcons .createShortcutIcon(pinnedShortcut, context); + if (pmHelper.isAppSuspended( + pinnedShortcut.getPackage(), info.user)) { + info.isDisabled |= ShortcutInfo.FLAG_DISABLED_SUSPENDED; + } intent = info.intent; } else { // Create a shortcut info in disabled mode for now. @@ -1044,7 +1016,7 @@ public class LauncherModel extends BroadcastReceiver info = c.loadSimpleShortcut(); // Shortcuts are only available on the primary profile - if (pmHelper.isAppSuspended(targetPkg)) { + if (pmHelper.isAppSuspended(targetPkg, c.user)) { disabledState |= ShortcutInfo.FLAG_DISABLED_SUSPENDED; } @@ -1258,17 +1230,23 @@ public class LauncherModel extends BroadcastReceiver } } - // Sort all the folder items and make sure the first 3 items are high resolution. + FolderIconPreviewVerifier verifier = + new FolderIconPreviewVerifier(mApp.getInvariantDeviceProfile()); + // Sort the folder items and make sure all items in the preview are high resolution. for (FolderInfo folder : sBgDataModel.folders) { Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR); - int pos = 0; + verifier.setFolderInfo(folder); + + int numItemsInPreview = 0; for (ShortcutInfo info : folder.contents) { - if (info.usingLowResIcon && - info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { + if (info.usingLowResIcon + && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION + && verifier.isItemInPreview(info.rank)) { mIconCache.getTitleAndIcon(info, false); + numItemsInPreview++; } - pos ++; - if (pos >= FolderIcon.NUM_ITEMS_IN_PREVIEW) { + + if (numItemsInPreview >= FolderIcon.NUM_ITEMS_IN_PREVIEW) { break; } } @@ -1393,7 +1371,7 @@ public class LauncherModel extends BroadcastReceiver return Utilities.longCompare(lhs.screenId, rhs.screenId); } default: - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { throw new RuntimeException("Unexpected container type when " + "sorting workspace items."); } @@ -1418,7 +1396,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } private void bindWorkspaceItems(final Callbacks oldCallbacks, @@ -1524,11 +1502,11 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); bindWorkspaceScreens(oldCallbacks, orderedScreenIds); - Executor mainExecutor = new DeferredMainThreadExecutor(); + Executor mainExecutor = mUiExecutor; // Load items on the current page. bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets, mainExecutor); @@ -1538,7 +1516,7 @@ public class LauncherModel extends BroadcastReceiver // This ensures that the first screen is immediately visible (eg. during rotation) // In case of !validFirstPage, bind all pages one after other. final Executor deferredExecutor = - validFirstPage ? new ViewOnDrawExecutor(mHandler) : mainExecutor; + validFirstPage ? new ViewOnDrawExecutor(mUiExecutor) : mainExecutor; mainExecutor.execute(new Runnable() { @Override @@ -1598,30 +1576,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); - } - } - - private void loadAndBindAllApps() { - if (DEBUG_LOADERS) { - Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded); - } - if (!mAllAppsLoaded) { - loadAllApps(); - synchronized (LoaderTask.this) { - if (mStopped) { - return; - } - } - updateIconCache(); - synchronized (LoaderTask.this) { - if (mStopped) { - return; - } - mAllAppsLoaded = true; - } - } else { - onlyBindAllApps(); + mUiExecutor.execute(r); } } @@ -1671,7 +1626,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } private void loadAllApps() { @@ -1719,21 +1674,21 @@ public class LauncherModel extends BroadcastReceiver 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); - } - } - }); + mUiExecutor.execute(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); + } + } + }); } } // Huh? Shouldn't this be inside the Runnable below? @@ -1741,7 +1696,7 @@ public class LauncherModel extends BroadcastReceiver mBgAllAppsList.added = new ArrayList<AppInfo>(); // Post callback on main thread - mHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { public void run() { final long bindTime = SystemClock.uptimeMillis(); @@ -1765,11 +1720,8 @@ public class LauncherModel extends BroadcastReceiver } } - private void loadAndBindDeepShortcuts() { - if (DEBUG_LOADERS) { - Log.d(TAG, "loadAndBindDeepShortcuts mDeepShortcutsLoaded=" + mDeepShortcutsLoaded); - } - if (!mDeepShortcutsLoaded) { + private void loadDeepShortcuts() { + if (!mModelLoaded) { sBgDataModel.deepShortcutMap.clear(); DeepShortcutManager shortcutManager = DeepShortcutManager.getInstance(mContext); mHasShortcutHostPermission = shortcutManager.hasHostPermission(); @@ -1782,14 +1734,7 @@ public class LauncherModel extends BroadcastReceiver } } } - synchronized (LoaderTask.this) { - if (mStopped) { - return; - } - mDeepShortcutsLoaded = true; - } } - bindDeepShortcuts(); } } @@ -1805,7 +1750,7 @@ public class LauncherModel extends BroadcastReceiver } } }; - runOnMainThread(r); + mUiExecutor.execute(r); } /** @@ -1831,6 +1776,12 @@ public class LauncherModel extends BroadcastReceiver } void enqueueModelUpdateTask(BaseModelUpdateTask task) { + if (!mModelLoaded && mLoaderTask == null) { + if (DEBUG_LOADERS) { + Log.d(TAG, "enqueueModelUpdateTask Ignoring task since loader is pending=" + task); + } + return; + } task.init(this); runOnWorkerThread(task); } @@ -1850,12 +1801,12 @@ public class LauncherModel extends BroadcastReceiver public static abstract class BaseModelUpdateTask implements Runnable { private LauncherModel mModel; - private DeferredHandler mUiHandler; + private Executor mUiExecutor; /* package private */ void init(LauncherModel model) { mModel = model; - mUiHandler = mModel.mHandler; + mUiExecutor = mModel.mUiExecutor; } @Override @@ -1878,7 +1829,7 @@ public class LauncherModel extends BroadcastReceiver */ public final void scheduleCallbackTask(final CallbackTask task) { final Callbacks callbacks = mModel.getCallback(); - mUiHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { public void run() { Callbacks cb = mModel.getCallback(); if (callbacks == cb && cb != null) { @@ -1923,7 +1874,7 @@ public class LauncherModel extends BroadcastReceiver private void bindWidgetsModel(final Callbacks callbacks) { final MultiHashMap<PackageItemInfo, WidgetItem> widgets = mBgWidgetsModel.getWidgetsMap().clone(); - mHandler.post(new Runnable() { + mUiExecutor.execute(new Runnable() { @Override public void run() { Callbacks cb = getCallback(); @@ -1980,14 +1931,6 @@ public class LauncherModel extends BroadcastReceiver } } - @Thunk class DeferredMainThreadExecutor implements Executor { - - @Override - public void execute(Runnable command) { - runOnMainThread(command); - } - } - /** * @return the looper for the worker thread which can be used to start background tasks. */ diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index e71ef2ce5..b83ddb927 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -53,7 +53,6 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.WorkspaceScreens; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.dynamicui.ExtractionUtils; import com.android.launcher3.logging.FileLog; import com.android.launcher3.provider.LauncherDbUtils; @@ -63,6 +62,8 @@ import com.android.launcher3.util.NoLocaleSqliteContext; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Thunk; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; @@ -74,7 +75,7 @@ public class LauncherProvider extends ContentProvider { private static final int DATABASE_VERSION = 27; - public static final String AUTHORITY = ProviderConfig.AUTHORITY; + public static final String AUTHORITY = (BuildConfig.APPLICATION_ID + ".settings").intern(); static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED"; @@ -85,9 +86,21 @@ public class LauncherProvider extends ContentProvider { protected DatabaseHelper mOpenHelper; + /** + * $ adb shell dumpsys activity provider com.android.launcher3 + */ + @Override + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + if (appState == null || !appState.getModel().isModelLoaded()) { + return; + } + appState.getModel().dumpState("", fd, writer, args); + } + @Override public boolean onCreate() { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { Log.d(TAG, "Launcher process started"); } mListenerHandler = new Handler(mListenerWrapper); @@ -174,7 +187,7 @@ public class LauncherProvider extends ContentProvider { if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) { LauncherAppState app = LauncherAppState.getInstanceNoCreate(); if (app != null) { - app.reloadWorkspace(); + app.getModel().forceReload(); } } } @@ -205,7 +218,7 @@ public class LauncherProvider extends ContentProvider { // Deprecated behavior to support legacy devices which rely on provider callbacks. LauncherAppState app = LauncherAppState.getInstanceNoCreate(); if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) { - app.reloadWorkspace(); + app.getModel().forceReload(); } String notify = uri.getQueryParameter("notify"); @@ -406,18 +419,13 @@ public class LauncherProvider extends ContentProvider { return result; } case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: { - createEmptyDB(); + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); return null; } case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: { loadDefaultFavoritesIfNecessary(); return null; } - case LauncherSettings.Settings.METHOD_DELETE_DB: { - // Are you sure? (y/n) - mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); - return null; - } } return null; } @@ -469,13 +477,6 @@ public class LauncherProvider extends ContentProvider { values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis()); } - /** - * Clears all the data for a fresh start. - */ - synchronized private void createEmptyDB() { - mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); - } - private void clearFlagEmptyDbCreated() { Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit(); } @@ -518,12 +519,12 @@ public class LauncherProvider extends ContentProvider { // There might be some partially restored DB items, due to buggy restore logic in // previous versions of launcher. - createEmptyDB(); + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); // Populate favorites table with initial favorites if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0) && usingExternallyProvidedLayout) { // Unable to load external layout. Cleanup and load the internal layout. - createEmptyDB(); + mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), getDefaultLayoutParser(widgetHost)); } @@ -825,9 +826,15 @@ public class LauncherProvider extends ContentProvider { * Clears all the data for a fresh start. */ public void createEmptyDB(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME); - db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME); - onCreate(db); + db.beginTransaction(); + try { + db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME); + onCreate(db); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } } /** diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index e450785c7..e8e0eb234 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -22,8 +22,6 @@ import android.net.Uri; import android.os.Bundle; import android.provider.BaseColumns; -import com.android.launcher3.config.ProviderConfig; - /** * Settings related utilities. */ @@ -101,7 +99,7 @@ public class LauncherSettings { * The content:// style URL for this table */ public static final Uri CONTENT_URI = Uri.parse("content://" + - ProviderConfig.AUTHORITY + "/" + TABLE_NAME); + LauncherProvider.AUTHORITY + "/" + TABLE_NAME); /** * The rank of this screen -- ie. how it is ordered relative to the other screens. @@ -121,7 +119,7 @@ public class LauncherSettings { * The content:// style URL for this table */ public static final Uri CONTENT_URI = Uri.parse("content://" + - ProviderConfig.AUTHORITY + "/" + TABLE_NAME); + LauncherProvider.AUTHORITY + "/" + TABLE_NAME); /** * The content:// style URL for a given row, identified by its id. @@ -131,7 +129,7 @@ public class LauncherSettings { * @return The unique content URL for the specified row. */ public static Uri getContentUri(long id) { - return Uri.parse("content://" + ProviderConfig.AUTHORITY + + return Uri.parse("content://" + LauncherProvider.AUTHORITY + "/" + TABLE_NAME + "/" + id); } @@ -280,7 +278,7 @@ public class LauncherSettings { public static final class Settings { public static final Uri CONTENT_URI = Uri.parse("content://" + - ProviderConfig.AUTHORITY + "/settings"); + LauncherProvider.AUTHORITY + "/settings"); public static final String METHOD_CLEAR_EMPTY_DB_FLAG = "clear_empty_db_flag"; public static final String METHOD_WAS_EMPTY_DB_CREATED = "get_empty_db_flag"; @@ -291,7 +289,6 @@ public class LauncherSettings { public static final String METHOD_NEW_SCREEN_ID = "generate_new_screen_id"; public static final String METHOD_CREATE_EMPTY_DB = "create_empty_db"; - public static final String METHOD_DELETE_DB = "delete_db"; public static final String METHOD_LOAD_DEFAULT_FAVORITES = "load_default_favorites"; diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java index 39c466db8..f5af979ce 100644 --- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java +++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java @@ -32,7 +32,7 @@ import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.anim.AnimationLayerSet; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.util.CircleRevealOutlineProvider; +import com.android.launcher3.anim.CircleRevealOutlineProvider; import com.android.launcher3.util.Thunk; import com.android.launcher3.widget.WidgetsContainerView; diff --git a/src/com/android/launcher3/MainThreadExecutor.java b/src/com/android/launcher3/MainThreadExecutor.java index 4ca0a59d8..509468233 100644 --- a/src/com/android/launcher3/MainThreadExecutor.java +++ b/src/com/android/launcher3/MainThreadExecutor.java @@ -18,14 +18,14 @@ package com.android.launcher3; import android.os.Looper; -import com.android.launcher3.util.LooperExecuter; +import com.android.launcher3.util.LooperExecutor; /** * An executor service that executes its tasks on the main thread. * * Shutting down this executor is not supported. */ -public class MainThreadExecutor extends LooperExecuter { +public class MainThreadExecutor extends LooperExecutor { public MainThreadExecutor() { super(Looper.getMainLooper()); diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java new file mode 100644 index 000000000..e8bf0a5c3 --- /dev/null +++ b/src/com/android/launcher3/SessionCommitReceiver.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2008 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.SessionInfo; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; + +import com.android.launcher3.compat.LauncherAppsCompat; + +import java.util.List; + +/** + * BroadcastReceiver to handle session commit intent. + */ +public class SessionCommitReceiver extends BroadcastReceiver { + + // Preference key for automatically adding icon to homescreen. + public static final String ADD_ICON_PREFERENCE_KEY = "pref_add_icon_to_home"; + + @Override + public void onReceive(Context context, Intent intent) { + if (!isEnabled(context)) { + // User has decided to not add icons on homescreen. + return; + } + + SessionInfo info = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION); + UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); + // TODO: Verify install reason + if (TextUtils.isEmpty(info.getAppPackageName())) { + return; + } + + if (!Process.myUserHandle().equals(user)) { + // Managed profile is handled using ManagedProfileHeuristic + return; + } + + List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(context) + .getActivityList(info.getAppPackageName(), user); + if (activities == null || activities.isEmpty()) { + // no activity found + return; + } + InstallShortcutReceiver.queueActivityInfo(activities.get(0), context); + } + + public static boolean isEnabled(Context context) { + return Utilities.getPrefs(context).getBoolean(ADD_ICON_PREFERENCE_KEY, true); + } +} diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java index cedeb3967..552e24ae4 100644 --- a/src/com/android/launcher3/SettingsActivity.java +++ b/src/com/android/launcher3/SettingsActivity.java @@ -25,6 +25,7 @@ import android.preference.Preference; import android.preference.PreferenceFragment; import android.provider.Settings; import android.provider.Settings.System; +import android.support.v4.os.BuildCompat; /** * Settings activity for Launcher. Currently implements the following setting: Allow rotation @@ -72,6 +73,11 @@ public class SettingsActivity extends Activity { mRotationLockObserver.onChange(true); rotationPref.setDefaultValue(Utilities.getAllowRotationDefaultValue(getActivity())); } + + if (!BuildCompat.isAtLeastO()) { + getPreferenceScreen().removePreference( + findPreference(SessionCommitReceiver.ADD_ICON_PREFERENCE_KEY)); + } } @Override diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java index b35dcb716..f0bb1c0c1 100644 --- a/src/com/android/launcher3/ShortcutInfo.java +++ b/src/com/android/launcher3/ShortcutInfo.java @@ -134,7 +134,6 @@ public class ShortcutInfo extends ItemInfoWithIcon { title = info.title; intent = new Intent(info.intent); iconResource = info.iconResource; - iconBitmap = info.iconBitmap; status = info.status; mInstallProgress = info.mInstallProgress; isDisabled = info.isDisabled; @@ -146,8 +145,6 @@ public class ShortcutInfo extends ItemInfoWithIcon { title = Utilities.trim(info.title); intent = new Intent(info.intent); isDisabled = info.isDisabled; - iconBitmap = info.iconBitmap; - usingLowResIcon = info.usingLowResIcon; } /** diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java index e68a5b030..0fac29f30 100644 --- a/src/com/android/launcher3/UninstallDropTarget.java +++ b/src/com/android/launcher3/UninstallDropTarget.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; @@ -139,9 +140,10 @@ public class UninstallDropTarget extends ButtonDropTarget { final Runnable checkIfUninstallWasSuccess = new Runnable() { @Override public void run() { - String packageName = cn.getPackageName(); - boolean uninstallSuccessful = !AllAppsList.packageHasActivities( - launcher, packageName, user); + // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well. + boolean uninstallSuccessful = LauncherAppsCompat.getInstance(launcher) + .getApplicationInfo(cn.getPackageName(), + PackageManager.MATCH_UNINSTALLED_PACKAGES, user) == null; callback.onDragObjectRemoved(uninstallSuccessful); } }; diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index abc53673b..2413d8ae6 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -51,7 +51,7 @@ import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -575,7 +575,7 @@ public final class Utilities { try { c.close(); } catch (IOException e) { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { Log.d(TAG, "Error closing", e); } } diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index 6d52ea3da..8f8d32ccc 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -65,7 +65,6 @@ import com.android.launcher3.anim.AnimationLayerSet; import com.android.launcher3.badge.FolderBadgeInfo; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; @@ -642,7 +641,8 @@ public class Workspace extends PagedView // of workspace despite that it's not a true child. // Note that it relies on the strict ordering of measuring the workspace before the QSB // at the dragLayer level. - if (getChildCount() > 0) { + // Only measure the QSB when the view is enabled + if (FeatureFlags.QSB_ON_FIRST_SCREEN && getChildCount() > 0) { CellLayout firstPage = (CellLayout) getChildAt(0); int cellHeight = firstPage.getCellHeight(); @@ -2617,7 +2617,7 @@ public class Workspace extends PagedView CellLayout parentCell = getParentCellLayoutForView(cell); if (parentCell != null) { parentCell.removeView(cell); - } else if (ProviderConfig.IS_DOGFOOD_BUILD) { + } else if (FeatureFlags.IS_DOGFOOD_BUILD) { throw new NullPointerException("mDragInfo.cell has null parent"); } addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1], @@ -2950,7 +2950,7 @@ public class Workspace extends PagedView ItemInfo item = d.dragInfo; if (item == null) { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { throw new NullPointerException("DragObject has null info"); } return; @@ -3607,7 +3607,7 @@ public class Workspace extends PagedView mDragInfo.container, mDragInfo.screenId); if (cellLayout != null) { cellLayout.onDropChild(mDragInfo.cell); - } else if (ProviderConfig.IS_DOGFOOD_BUILD) { + } else if (FeatureFlags.IS_DOGFOOD_BUILD) { throw new RuntimeException("Invalid state: cellLayout == null in " + "Workspace#onDropCompleted. Please file a bug. "); }; @@ -3633,7 +3633,7 @@ public class Workspace extends PagedView CellLayout parentCell = getParentCellLayoutForView(v); if (parentCell != null) { parentCell.removeView(v); - } else if (ProviderConfig.IS_DOGFOOD_BUILD) { + } else if (FeatureFlags.IS_DOGFOOD_BUILD) { // 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 @@ -3884,7 +3884,9 @@ public class Workspace extends PagedView // The item may belong to a folder. View parent = idToViewMap.get(itemToRemove.container); if (parent != null) { - ((FolderInfo) parent.getTag()).remove((ShortcutInfo) itemToRemove, false); + FolderInfo folderInfo = (FolderInfo) parent.getTag(); + folderInfo.prepareAutoUpdate(); + folderInfo.remove((ShortcutInfo) itemToRemove, false); } } } diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 0732004d4..cc5fa8ce1 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -20,6 +20,8 @@ import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.InsetDrawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.Spannable; @@ -46,6 +48,8 @@ import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.discovery.AppDiscoveryItem; +import com.android.launcher3.discovery.AppDiscoveryUpdateState; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.folder.Folder; @@ -211,7 +215,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc // IF scroller is at the very top OR there is no scroll bar because there is probably not // enough items to scroll, THEN it's okay for the container to be pulled down. - if (mAppsRecyclerView.getScrollBar().getThumbOffsetY() <= 0) { + if (mAppsRecyclerView.getCurrentScrollY() == 0) { return true; } return false; @@ -425,14 +429,22 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc @Override public void onSearchResult(String query, ArrayList<ComponentKey> apps) { if (apps != null) { - if (mApps.setOrderedFilter(apps)) { - mAppsRecyclerView.onSearchResultsChanged(); - } + mApps.setOrderedFilter(apps); + mAppsRecyclerView.onSearchResultsChanged(); mAdapter.setLastSearchQuery(query); } } @Override + public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, + @NonNull AppDiscoveryUpdateState state) { + if (!mLauncher.isDestroyed()) { + mApps.onAppDiscoverySearchUpdate(app, state); + mAppsRecyclerView.onSearchResultsChanged(); + } + } + + @Override public void clearSearchResult() { if (mApps.setOrderedFilter(null)) { mAppsRecyclerView.onSearchResultsChanged(); diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java index 28b7685ed..a1ff8223a 100644 --- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java +++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java @@ -19,6 +19,7 @@ import android.support.v7.widget.RecyclerView; import android.view.View; import com.android.launcher3.BaseRecyclerViewFastScrollBar; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.FastBitmapDrawable; import com.android.launcher3.util.Thunk; @@ -45,8 +46,7 @@ public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallb // Set of all views animated during fast scroll. We keep track of these ourselves since there // is no way to reset a view once it gets scrapped or recycled without other hacks - private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews = - new HashSet<>(); + private HashSet<RecyclerView.ViewHolder> mTrackedFastScrollViews = new HashSet<>(); // Smooth fast-scroll animation frames @Thunk int mFastScrollFrameIndex; @@ -186,12 +186,7 @@ public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallb public void onBindView(AllAppsGridAdapter.ViewHolder holder) { // Update newly bound views to the current fast scroll state if we are fast scrolling if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) { - if (holder.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { - BaseRecyclerViewFastScrollBar.FastScrollFocusableView v = - (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.itemView; - updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */); - mTrackedFastScrollViews.add(v); - } + mTrackedFastScrollViews.add(holder); } } @@ -201,9 +196,9 @@ public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallb private void trackAllChildViews() { int childCount = mRv.getChildCount(); for (int i = 0; i < childCount; i++) { - View v = mRv.getChildAt(i); - if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) { - mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v); + RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder(mRv.getChildAt(i)); + if (viewHolder != null) { + mTrackedFastScrollViews.add(viewHolder); } } } @@ -212,27 +207,16 @@ public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallb * Updates the fast scroll focus on all the children. */ private void updateTrackedViewsFastScrollFocusState() { - for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) { - RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v); - int pos = (viewHolder != null) ? viewHolder.getPosition() : -1; - updateViewFastScrollFocusState(v, pos, true); - } - } - - /** - * Updates the fast scroll focus on all a given view. - */ - private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v, - int pos, boolean animated) { - FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL; - if (mCurrentFastScrollSection != null && pos > -1) { - AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos); - boolean highlight = item.sectionName.equals(mCurrentFastScrollSection) && - item.position == mTargetFastScrollPosition; - newState = highlight ? - FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED : - FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED; + for (RecyclerView.ViewHolder viewHolder : mTrackedFastScrollViews) { + int pos = viewHolder.getAdapterPosition(); + boolean isActive = false; + if (mCurrentFastScrollSection != null && pos > -1) { + AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos); + isActive = item != null && + mCurrentFastScrollSection.equals(item.sectionName) && + item.position == mTargetFastScrollPosition; + } + viewHolder.itemView.setActivated(isActive); } - v.setFastScrollFocusState(newState, animated); } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index bd877f248..59cac8d26 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -19,8 +19,8 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; -import android.graphics.Rect; import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; @@ -33,11 +33,15 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.TextView; +import com.android.launcher3.discovery.AppDiscoveryAppInfo; import com.android.launcher3.AppInfo; import com.android.launcher3.BubbleTextView; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; +import com.android.launcher3.discovery.AppDiscoveryItemView; + +import java.util.List; /** * The grid view adapter of all the apps. @@ -64,6 +68,8 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6; // The divider that separates prediction icons from the app list public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7; + public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 8; + public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 9; // Common view type masks public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER @@ -71,6 +77,8 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. | VIEW_TYPE_PREDICTION_DIVIDER; public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON | VIEW_TYPE_PREDICTION_ICON; + public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON + | VIEW_TYPE_DISCOVERY_ITEM; public interface BindViewCallback { @@ -81,7 +89,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. * ViewHolder for each icon. */ public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(View v) { super(v); } @@ -105,17 +112,53 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. final AccessibilityRecordCompat record = AccessibilityEventCompat .asRecord(event); record.setItemCount(mApps.getNumFilteredApps()); + record.setFromIndex(Math.max(0, + record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); + record.setToIndex(Math.max(0, + record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); } @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); + return super.getRowCountForAccessibility(recycler, state) - + getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); + } + + @Override + public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, + RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); + + ViewGroup.LayoutParams lp = host.getLayoutParams(); + AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); + if (!(lp instanceof LayoutParams) || (cic == null)) { + return; } + LayoutParams glp = (LayoutParams) lp; + info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( + cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), + cic.getRowSpan(), + cic.getColumnIndex(), + cic.getColumnSpan(), + cic.isHeading(), + cic.isSelected())); + } + + /** + * Returns the number of rows before {@param adapterPosition}, including this position + * which should not be counted towards the collection info. + */ + private int getRowsNotForAccessibility(int adapterPosition) { + List<AdapterItem> items = mApps.getAdapterItems(); + adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); + int extraRows = 0; + for (int i = 0; i <= adapterPosition; i++) { + if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) { + extraRows++; + } + } + return extraRows; } @Override @@ -234,8 +277,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_ICON: - /* falls through */ - case VIEW_TYPE_PREDICTION_ICON: { + case VIEW_TYPE_PREDICTION_ICON: BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( R.layout.all_apps_icon, parent, false); icon.setOnClickListener(mIconClickListener); @@ -245,14 +287,14 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. icon.setOnFocusChangeListener(mIconFocusListener); // Ensure the all apps icon height matches the workspace icons - DeviceProfile profile = mLauncher.getDeviceProfile(); - Point cellSize = profile.getCellSize(); - GridLayoutManager.LayoutParams lp = - (GridLayoutManager.LayoutParams) icon.getLayoutParams(); - lp.height = cellSize.y; - icon.setLayoutParams(lp); + icon.getLayoutParams().height = getCellSize().y; return new ViewHolder(icon); - } + case VIEW_TYPE_DISCOVERY_ITEM: + AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater + .inflate(R.layout.all_apps_discovery_item, parent, false); + appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(), + mIconLongClickListener); + return new ViewHolder(appDiscoveryItemView); case VIEW_TYPE_EMPTY_SEARCH: return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent, false)); @@ -269,8 +311,11 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. case VIEW_TYPE_SEARCH_DIVIDER: return new ViewHolder(mLayoutInflater.inflate( R.layout.all_apps_search_divider, parent, false)); + case VIEW_TYPE_APPS_LOADING_DIVIDER: + View loadingDividerView = mLayoutInflater.inflate( + R.layout.all_apps_discovery_loading_divider, parent, false); + return new ViewHolder(loadingDividerView); case VIEW_TYPE_PREDICTION_DIVIDER: - /* falls through */ case VIEW_TYPE_SEARCH_MARKET_DIVIDER: return new ViewHolder(mLayoutInflater.inflate( R.layout.all_apps_divider, parent, false)); @@ -279,23 +324,26 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. } } + private Point getCellSize() { + return mLauncher.getDeviceProfile().getCellSize(); + } + @Override public void onBindViewHolder(ViewHolder holder, int position) { switch (holder.getItemViewType()) { - case VIEW_TYPE_ICON: { + case VIEW_TYPE_ICON: + case VIEW_TYPE_PREDICTION_ICON: AppInfo info = mApps.getAdapterItems().get(position).appInfo; BubbleTextView icon = (BubbleTextView) holder.itemView; icon.applyFromApplicationInfo(info); icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); break; - } - case VIEW_TYPE_PREDICTION_ICON: { - AppInfo info = mApps.getAdapterItems().get(position).appInfo; - BubbleTextView icon = (BubbleTextView) holder.itemView; - icon.applyFromApplicationInfo(info); - icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); + case VIEW_TYPE_DISCOVERY_ITEM: + AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo) + mApps.getAdapterItems().get(position).appInfo; + AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView; + view.apply(appDiscoveryAppInfo); break; - } case VIEW_TYPE_EMPTY_SEARCH: TextView emptyViewText = (TextView) holder.itemView; emptyViewText.setText(mEmptySearchMessage); @@ -310,6 +358,15 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. searchView.setVisibility(View.GONE); } break; + case VIEW_TYPE_APPS_LOADING_DIVIDER: + int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; + int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; + holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading); + holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded); + break; + case VIEW_TYPE_SEARCH_MARKET_DIVIDER: + // nothing to do + break; } if (mBindViewCallback != null) { mBindViewCallback.onBindView(holder); diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index a41d83244..64e2fcb3d 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -30,6 +30,7 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.DrawableFactory; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; @@ -110,44 +111,40 @@ public class AllAppsRecyclerView extends BaseRecyclerView { * all the different view types. */ public void preMeasureViews(AllAppsGridAdapter adapter) { + View icon = adapter.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON).itemView; + final int iconHeight = icon.getLayoutParams().height; + mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight); + mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight); + final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST); final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST); - // Icons - BubbleTextView icon = (BubbleTextView) adapter.onCreateViewHolder(this, - AllAppsGridAdapter.VIEW_TYPE_ICON).itemView; - int iconHeight = icon.getLayoutParams().height; - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight); + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, + AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER); + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER); + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET); + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH); + + if (FeatureFlags.DISCOVERY_ENABLED) { + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER); + putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec, + AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM); + } + } - // Search divider - View searchDivider = adapter.onCreateViewHolder(this, - AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER).itemView; - searchDivider.measure(widthMeasureSpec, heightMeasureSpec); - int searchDividerHeight = searchDivider.getMeasuredHeight(); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER, searchDividerHeight); - - // Generic dividers - View divider = adapter.onCreateViewHolder(this, - AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER).itemView; - divider.measure(widthMeasureSpec, heightMeasureSpec); - int dividerHeight = divider.getMeasuredHeight(); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, dividerHeight); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, dividerHeight); - - // Search views - View emptySearch = adapter.onCreateViewHolder(this, - AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH).itemView; - emptySearch.measure(widthMeasureSpec, heightMeasureSpec); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, - emptySearch.getMeasuredHeight()); - View searchMarket = adapter.onCreateViewHolder(this, - AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET).itemView; - searchMarket.measure(widthMeasureSpec, heightMeasureSpec); - mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, - searchMarket.getMeasuredHeight()); + private void putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes) { + View view = adapter.onCreateViewHolder(this, viewTypes[0]).itemView; + view.measure(w, h); + for (int viewType : viewTypes) { + mViewHeights.put(viewType, view.getMeasuredHeight()); + } } /** @@ -207,7 +204,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView { // Always scroll the view to the top so the user can see the changed results scrollToTop(); - if (mApps.hasNoFilteredResults()) { + if (mApps.shouldShowEmptySearch()) { if (mEmptySearchBackground == null) { mEmptySearchBackground = DrawableFactory.get(getContext()) .getAllAppsBackground(getContext()); @@ -438,4 +435,5 @@ public class AllAppsRecyclerView extends BaseRecyclerView { x + mEmptySearchBackground.getIntrinsicWidth(), y + mEmptySearchBackground.getIntrinsicHeight()); } + } diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java index 365ab3185..c7ba3abc6 100644 --- a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java +++ b/src/com/android/launcher3/allapps/AllAppsSearchBarController.java @@ -19,6 +19,8 @@ import android.content.Context; import android.content.Intent; import android.graphics.Rect; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -32,6 +34,8 @@ import android.widget.TextView.OnEditorActionListener; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Launcher; import com.android.launcher3.Utilities; +import com.android.launcher3.discovery.AppDiscoveryItem; +import com.android.launcher3.discovery.AppDiscoveryUpdateState; import com.android.launcher3.util.ComponentKey; import java.util.ArrayList; @@ -46,7 +50,7 @@ public abstract class AllAppsSearchBarController protected AlphabeticalAppsList mApps; protected Callbacks mCb; protected ExtendedEditText mInput; - private String mQuery; + protected String mQuery; protected DefaultAppSearchAlgorithm mSearchAlgorithm; protected InputMethodManager mInputMethodManager; @@ -73,6 +77,14 @@ public abstract class AllAppsSearchBarController mInput.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mSearchAlgorithm = onInitializeSearch(); + + onInitialized(); + } + + /** + * You can override this method to perform custom initialization. + */ + protected void onInitialized() { } /** @@ -117,6 +129,7 @@ public abstract class AllAppsSearchBarController if (actionId != EditorInfo.IME_ACTION_SEARCH) { return false; } + // Skip if the query is empty String query = v.getText().toString(); if (query.isEmpty()) { @@ -206,5 +219,19 @@ public abstract class AllAppsSearchBarController * Called when the search results should be cleared. */ void clearSearchResult(); + + + /** + * Called when the app discovery is providing an update of search, which can either be + * START for starting a new discovery, + * UPDATE for providing a new search result, can be called multiple times, + * END for indicating the end of results. + * + * @param app result item if UPDATE, else null + * @param app the update state, START, UPDATE or END + */ + void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, + @NonNull AppDiscoveryUpdateState state); } + }
\ No newline at end of file diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 0bfbd3eba..9e32d257d 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -17,12 +17,17 @@ package com.android.launcher3.allapps; import android.content.Context; import android.os.Process; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import com.android.launcher3.AppInfo; import com.android.launcher3.Launcher; import com.android.launcher3.compat.AlphabeticIndexCompat; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.discovery.AppDiscoveryAppInfo; +import com.android.launcher3.discovery.AppDiscoveryItem; +import com.android.launcher3.discovery.AppDiscoveryUpdateState; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.LabelComparator; @@ -48,6 +53,8 @@ public class AlphabeticalAppsList { private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; + private AppDiscoveryUpdateState mAppDiscoveryUpdateState; + /** * Info about a fast scroller section, depending if sections are merged, the fast scroller * sections will not be the same set as the section headers. @@ -106,6 +113,17 @@ public class AlphabeticalAppsList { return item; } + public static AdapterItem asDiscoveryItem(int pos, String sectionName, AppInfo appInfo, + int appIndex) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM; + item.position = pos; + item.sectionName = sectionName; + item.appInfo = appInfo; + item.appIndex = appIndex; + return item; + } + public static AdapterItem asEmptySearch(int pos) { AdapterItem item = new AdapterItem(); item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH; @@ -134,6 +152,13 @@ public class AlphabeticalAppsList { return item; } + public static AdapterItem asLoadingDivider(int pos) { + AdapterItem item = new AdapterItem(); + item.viewType = AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER; + item.position = pos; + return item; + } + public static AdapterItem asMarketSearch(int pos) { AdapterItem item = new AdapterItem(); item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET; @@ -142,22 +167,24 @@ public class AlphabeticalAppsList { } } - private Launcher mLauncher; + private final Launcher mLauncher; // The set of apps from the system not including predictions private final List<AppInfo> mApps = new ArrayList<>(); private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>(); // The set of filtered apps with the current filter - private List<AppInfo> mFilteredApps = new ArrayList<>(); + private final List<AppInfo> mFilteredApps = new ArrayList<>(); // The current set of adapter items - private List<AdapterItem> mAdapterItems = new ArrayList<>(); + private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>(); // The set of sections that we allow fast-scrolling to (includes non-merged sections) - private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); + private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); // The set of predicted app component names - private List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); + private final List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); // The set of predicted apps resolved from the component names and the current set of apps - private List<AppInfo> mPredictedApps = new ArrayList<>(); + private final List<AppInfo> mPredictedApps = new ArrayList<>(); + private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>(); + // The of ordered component names as a result of a search query private ArrayList<ComponentKey> mSearchResults; private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); @@ -240,6 +267,10 @@ public class AlphabeticalAppsList { return (mSearchResults != null) && mFilteredApps.isEmpty(); } + boolean shouldShowEmptySearch() { + return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty(); + } + /** * Sets the sorted list of filtered components. */ @@ -253,6 +284,20 @@ public class AlphabeticalAppsList { return false; } + public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, + @NonNull AppDiscoveryUpdateState state) { + mAppDiscoveryUpdateState = state; + switch (state) { + case START: + mDiscoveredApps.clear(); + break; + case UPDATE: + mDiscoveredApps.add(new AppDiscoveryAppInfo(app, mLauncher)); + break; + } + updateAdapterItems(); + } + /** * Sets the current set of predicted apps. Since this can be called before we get the full set * of applications, we should merge the results only in onAppsUpdated() which is idempotent. @@ -350,6 +395,17 @@ public class AlphabeticalAppsList { * mCachedSectionNames to have been calculated for the set of all apps in mApps. */ private void updateAdapterItems() { + refillAdapterItems(); + refreshRecyclerView(); + } + + private void refreshRecyclerView() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + + private void refillAdapterItems() { String lastSectionName = null; FastScrollSectionInfo lastFastScrollerSectionInfo = null; int position = 0; @@ -384,7 +440,7 @@ public class AlphabeticalAppsList { if (info != null) { mPredictedApps.add(info); } else { - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { Log.e(TAG, "Predicted app not found: " + ck); } } @@ -435,14 +491,30 @@ 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++)); + if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) { + mAdapterItems.add(AdapterItem.asLoadingDivider(position++)); + + // Append all app discovery results + for (int i = 0; i < mDiscoveredApps.size(); i++) { + AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i); + AdapterItem item = AdapterItem.asDiscoveryItem(position++, + "", appDiscoveryAppInfo, appIndex++); + mAdapterItems.add(item); + } + + if (!isAppDiscoveryRunning()) { + mAdapterItems.add(AdapterItem.asMarketSearch(position++)); + } } else { - mAdapterItems.add(AdapterItem.asMarketDivider(position++)); + // Append the search market item + if (hasNoFilteredResults()) { + mAdapterItems.add(AdapterItem.asEmptySearch(position++)); + } else { + mAdapterItems.add(AdapterItem.asMarketDivider(position++)); + } + mAdapterItems.add(AdapterItem.asMarketSearch(position++)); } - mAdapterItems.add(AdapterItem.asMarketSearch(position++)); } if (mNumAppsPerRow != 0) { @@ -498,11 +570,11 @@ public class AlphabeticalAppsList { break; } } + } - // Refresh the recycler view - if (mAdapter != null) { - mAdapter.notifyDataSetChanged(); - } + public boolean isAppDiscoveryRunning() { + return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START + || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE; } private List<AppInfo> getFiltersAppInfos() { @@ -532,4 +604,5 @@ public class AlphabeticalAppsList { } return sectionName; } + } diff --git a/src/com/android/launcher3/util/CircleRevealOutlineProvider.java b/src/com/android/launcher3/anim/CircleRevealOutlineProvider.java index 9fe51476d..9fb6b498b 100644 --- a/src/com/android/launcher3/util/CircleRevealOutlineProvider.java +++ b/src/com/android/launcher3/anim/CircleRevealOutlineProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.util; +package com.android.launcher3.anim; public class CircleRevealOutlineProvider extends RevealOutlineAnimation { diff --git a/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java b/src/com/android/launcher3/anim/PillHeightRevealOutlineProvider.java index 89dda3b26..679e8e32f 100644 --- a/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java +++ b/src/com/android/launcher3/anim/PillHeightRevealOutlineProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 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. @@ -14,28 +14,28 @@ * limitations under the License. */ -package com.android.launcher3.util; +package com.android.launcher3.anim; import android.graphics.Rect; /** - * Extension of {@link PillRevealOutlineProvider} which only changes the width of the pill. + * Extension of {@link PillRevealOutlineProvider} which only changes the height of the pill. + * For now, we assume the height is added/removed from the bottom. */ -public class PillWidthRevealOutlineProvider extends PillRevealOutlineProvider { +public class PillHeightRevealOutlineProvider extends PillRevealOutlineProvider { - private final int mStartLeft; - private final int mStartRight; + private final int mNewHeight; - public PillWidthRevealOutlineProvider(Rect pillRect, int left, int right) { - super(0, 0, pillRect); + public PillHeightRevealOutlineProvider(Rect pillRect, float radius, int newHeight) { + super(0, 0, pillRect, radius); mOutline.set(pillRect); - mStartLeft = left; - mStartRight = right; + mNewHeight = newHeight; } @Override public void setProgress(float progress) { - mOutline.left = (int) (progress * mPillRect.left + (1 - progress) * mStartLeft); - mOutline.right = (int) (progress * mPillRect.right + (1 - progress) * mStartRight); + mOutline.top = 0; + int heightDifference = mPillRect.height() - mNewHeight; + mOutline.bottom = (int) (mPillRect.bottom - heightDifference * (1 - progress)); } } diff --git a/src/com/android/launcher3/util/PillRevealOutlineProvider.java b/src/com/android/launcher3/anim/PillRevealOutlineProvider.java index a57d69fab..450f9db9a 100644 --- a/src/com/android/launcher3/util/PillRevealOutlineProvider.java +++ b/src/com/android/launcher3/anim/PillRevealOutlineProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.util; +package com.android.launcher3.anim; import android.graphics.Rect; import android.view.ViewOutlineProvider; diff --git a/src/com/android/launcher3/anim/PropertyResetListener.java b/src/com/android/launcher3/anim/PropertyResetListener.java new file mode 100644 index 000000000..eefb0148c --- /dev/null +++ b/src/com/android/launcher3/anim/PropertyResetListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2017 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.anim; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.Property; + +/** + * An AnimatorListener that sets the given property to the given value at the end of the animation. + */ +public class PropertyResetListener<T, V> extends AnimatorListenerAdapter { + + private Property<T, V> mPropertyToReset; + private V mResetToValue; + + public PropertyResetListener(Property<T, V> propertyToReset, V resetToValue) { + mPropertyToReset = propertyToReset; + mResetToValue = resetToValue; + } + + @Override + public void onAnimationEnd(Animator animation) { + mPropertyToReset.set((T) ((ObjectAnimator) animation).getTarget(), mResetToValue); + } +} diff --git a/src/com/android/launcher3/util/RevealOutlineAnimation.java b/src/com/android/launcher3/anim/RevealOutlineAnimation.java index 456047775..51d00d947 100644 --- a/src/com/android/launcher3/util/RevealOutlineAnimation.java +++ b/src/com/android/launcher3/anim/RevealOutlineAnimation.java @@ -1,4 +1,4 @@ -package com.android.launcher3.util; +package com.android.launcher3.anim; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -83,4 +83,8 @@ public abstract class RevealOutlineAnimation extends ViewOutlineProvider { public void getOutline(View v, Outline outline) { outline.setRoundRect(mOutline, mOutlineRadius); } + + public float getRadius() { + return mOutlineRadius; + } } diff --git a/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java b/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java new file mode 100644 index 000000000..9c09477bb --- /dev/null +++ b/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 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.anim; + +import android.graphics.Rect; + +/** + * A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii + * and two {@link Rect}s. + * + * An example usage of this provider is an outline that starts out as a circle and ends + * as a rounded rectangle. + */ +public class RoundedRectRevealOutlineProvider extends RevealOutlineAnimation { + private final float mStartRadius; + private final float mEndRadius; + + private final Rect mStartRect; + private final Rect mEndRect; + + public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect, + Rect endRect) { + mStartRadius = startRadius; + mEndRadius = endRadius; + mStartRect = startRect; + mEndRect = endRect; + } + + @Override + public boolean shouldRemoveElevationDuringAnimation() { + return false; + } + + @Override + public void setProgress(float progress) { + mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius; + + mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left); + mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top); + mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right); + mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom); + } +} diff --git a/src/com/android/launcher3/badge/BadgeRenderer.java b/src/com/android/launcher3/badge/BadgeRenderer.java index 8bbc2afa2..58969289e 100644 --- a/src/com/android/launcher3/badge/BadgeRenderer.java +++ b/src/com/android/launcher3/badge/BadgeRenderer.java @@ -20,14 +20,15 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Shader; import android.support.annotation.Nullable; import com.android.launcher3.R; import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.graphics.ShadowGenerator; /** * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). @@ -40,10 +41,12 @@ public class BadgeRenderer { private final int mTextHeight; private final IconDrawer mLargeIconDrawer; private final IconDrawer mSmallIconDrawer; - private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG + | Paint.FILTER_BITMAP_FLAG); + private final Bitmap mBackgroundWithShadow; - public BadgeRenderer(Context context) { + public BadgeRenderer(final Context context) { mContext = context; Resources res = context.getResources(); mSize = res.getDimensionPixelSize(R.dimen.badge_size); @@ -51,10 +54,13 @@ public class BadgeRenderer { mSmallIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_large_padding)); mTextPaint.setTextAlign(Paint.Align.CENTER); mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.badge_text_size)); + mTextPaint.setFakeBoldText(true); // Measure the text height. Rect tempTextHeight = new Rect(); mTextPaint.getTextBounds("0", 0, 1, tempTextHeight); mTextHeight = tempTextHeight.height(); + + mBackgroundWithShadow = ShadowGenerator.createCircleWithShadow(Color.WHITE, mSize); } /** @@ -67,13 +73,15 @@ public class BadgeRenderer { */ public void draw(Canvas canvas, IconPalette palette, @Nullable BadgeInfo badgeInfo, Rect iconBounds, float badgeScale) { - mBackgroundPaint.setColor(palette.backgroundColor); mTextPaint.setColor(palette.textColor); canvas.save(Canvas.MATRIX_SAVE_FLAG); // We draw the badge relative to its center. canvas.translate(iconBounds.right - mSize / 2, iconBounds.top + mSize / 2); canvas.scale(badgeScale, badgeScale); - canvas.drawCircle(0, 0, mSize / 2, mBackgroundPaint); + mBackgroundPaint.setColorFilter(palette.backgroundColorMatrixFilter); + int backgroundSize = mBackgroundWithShadow.getHeight(); // Same as width. + canvas.drawBitmap(mBackgroundWithShadow, -backgroundSize / 2, -backgroundSize / 2, + mBackgroundPaint); IconDrawer iconDrawer = badgeInfo != null && badgeInfo.isIconLarge() ? mLargeIconDrawer : mSmallIconDrawer; Shader icon = badgeInfo == null ? null : badgeInfo.getNotificationIconForBadge( diff --git a/src/com/android/launcher3/compat/LauncherAppsCompat.java b/src/com/android/launcher3/compat/LauncherAppsCompat.java index b9142ed16..2eb5b023b 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompat.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompat.java @@ -19,6 +19,7 @@ package com.android.launcher3.compat; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.graphics.Rect; import android.os.Bundle; @@ -72,6 +73,8 @@ public abstract class LauncherAppsCompat { UserHandle user); public abstract void startActivityForProfile(ComponentName component, UserHandle user, Rect sourceBounds, Bundle opts); + public abstract ApplicationInfo getApplicationInfo( + String packageName, int flags, UserHandle user); public abstract void showAppDetailsForProfile(ComponentName component, UserHandle user); public abstract void addOnAppsChangedCallback(OnAppsChangedCallbackCompat listener); public abstract void removeOnAppsChangedCallback(OnAppsChangedCallbackCompat listener); diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java index 3cb721ccc..459017392 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVL.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVL.java @@ -19,6 +19,7 @@ package com.android.launcher3.compat; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; @@ -26,6 +27,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.graphics.Rect; import android.os.Bundle; +import android.os.Process; import android.os.UserHandle; import com.android.launcher3.compat.ShortcutConfigActivityInfo.ShortcutConfigActivityInfoVL; @@ -65,6 +67,32 @@ public class LauncherAppsCompatVL extends LauncherAppsCompat { } @Override + public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user) { + final boolean isPrimaryUser = Process.myUserHandle().equals(user); + if (!isPrimaryUser && (flags == 0)) { + // We are looking for an installed app on a secondary profile. Prior to O, the only + // entry point for work profiles is through the LauncherActivity. + List<LauncherActivityInfo> activityList = + mLauncherApps.getActivityList(packageName, user); + return activityList.size() > 0 ? activityList.get(0).getApplicationInfo() : null; + } + try { + ApplicationInfo info = + mContext.getPackageManager().getApplicationInfo(packageName, flags); + // There is no way to check if the app is installed for managed profile. But for + // primary profile, we can still have this check. + if (isPrimaryUser && ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) + || !info.enabled) { + return null; + } + return info; + } catch (PackageManager.NameNotFoundException e) { + // Package not found + return null; + } + } + + @Override public void showAppDetailsForProfile(ComponentName component, UserHandle user) { mLauncherApps.startAppDetailsActivity(component, user, null, null); } diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java index 0610726a7..27433796a 100644 --- a/src/com/android/launcher3/compat/LauncherAppsCompatVO.java +++ b/src/com/android/launcher3/compat/LauncherAppsCompatVO.java @@ -17,6 +17,7 @@ package com.android.launcher3.compat; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.os.UserHandle; @@ -34,6 +35,13 @@ public class LauncherAppsCompatVO extends LauncherAppsCompatVL { } @Override + public ApplicationInfo getApplicationInfo(String packageName, int flags, UserHandle user) { + ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, flags, user); + return info == null || (info.flags & ApplicationInfo.FLAG_INSTALLED) == 0 || !info.enabled + ? null : info; + } + + @Override public List<ShortcutConfigActivityInfo> getCustomShortcutActivityList() { List<ShortcutConfigActivityInfo> result = new ArrayList<>(); diff --git a/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java b/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java new file mode 100644 index 000000000..50e979aac --- /dev/null +++ b/src/com/android/launcher3/discovery/AppDiscoveryAppInfo.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 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.discovery; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutInfo; + +public class AppDiscoveryAppInfo extends AppInfo { + + private final @NonNull Launcher mLauncher; + + public final boolean showAsDiscoveryItem; + public final boolean isInstantApp; + public final float rating; + public final long reviewCount; + public final @NonNull String publisher; + public final @NonNull Intent installIntent; + public final @NonNull Intent launchIntent; + public final @Nullable String priceFormatted; + + public AppDiscoveryAppInfo(AppDiscoveryItem item, Launcher launcher) { + this.mLauncher = launcher; + this.intent = item.isInstantApp ? item.launchIntent : item.installIntent; + this.title = item.title; + this.iconBitmap = item.bitmap; + this.isDisabled = ShortcutInfo.DEFAULT; + this.usingLowResIcon = false; + this.isInstantApp = item.isInstantApp; + this.rating = item.starRating; + this.showAsDiscoveryItem = true; + this.publisher = item.publisher != null ? item.publisher : ""; + this.priceFormatted = item.price; + this.componentName = new ComponentName(item.packageName, ""); + this.installIntent = item.installIntent; + this.launchIntent = item.launchIntent; + this.reviewCount = item.reviewCount; + this.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; + } + + @Override + public ShortcutInfo makeShortcut() { + if (!isDragAndDropSupported()) { + throw new RuntimeException("DnD is currently not supported for discovered store apps"); + } + ShortcutInfo shortcutInfo = super.makeShortcut(); + if (isInstantApp) { + int iconSize = iconBitmap.getWidth(); + int badgeSize = mLauncher.getResources().getDimensionPixelOffset(R.dimen.badge_size); + Bitmap icon = Bitmap.createBitmap(iconBitmap); + Drawable badgeDrawable = mLauncher.getDrawable(R.drawable.ic_instant_app); + badgeDrawable.setBounds(iconSize - badgeSize, iconSize - badgeSize, iconSize, iconSize); + Canvas canvas = new Canvas(icon); + badgeDrawable.draw(canvas); + shortcutInfo.iconBitmap = icon; + } + return shortcutInfo; + } + + public boolean isDragAndDropSupported() { + return isInstantApp; + } + +} diff --git a/src/com/android/launcher3/discovery/AppDiscoveryItem.java b/src/com/android/launcher3/discovery/AppDiscoveryItem.java new file mode 100644 index 000000000..7c10371d0 --- /dev/null +++ b/src/com/android/launcher3/discovery/AppDiscoveryItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 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.discovery; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; + +/** + * This class represents the model for a discovered app via app discovery. + * It holds all information for one result retrieved from an app discovery service. + */ +public class AppDiscoveryItem { + + public final String packageName; + public final boolean isInstantApp; + public final float starRating; + public final long reviewCount; + public final Intent launchIntent; + public final Intent installIntent; + public final CharSequence title; + public final String publisher; + public final String price; + public final Bitmap bitmap; + + public AppDiscoveryItem(String packageName, + boolean isInstantApp, + float starRating, + long reviewCount, + CharSequence title, + String publisher, + Bitmap bitmap, + String price, + Intent launchIntent, + Intent installIntent) { + this.packageName = packageName; + this.isInstantApp = isInstantApp; + this.starRating = starRating; + this.reviewCount = reviewCount; + this.launchIntent = launchIntent; + this.installIntent = installIntent; + this.title = title; + this.publisher = publisher; + this.price = price; + this.bitmap = bitmap; + } + +} diff --git a/src/com/android/launcher3/discovery/AppDiscoveryItemView.java b/src/com/android/launcher3/discovery/AppDiscoveryItemView.java new file mode 100644 index 000000000..6faad87ab --- /dev/null +++ b/src/com/android/launcher3/discovery/AppDiscoveryItemView.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 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.discovery; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.android.launcher3.R; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +public class AppDiscoveryItemView extends RelativeLayout { + + private static boolean SHOW_REVIEW_COUNT = false; + + private ImageView mImage; + private ImageView mBadge; + private TextView mTitle; + private TextView mRatingText; + private RatingView mRatingView; + private TextView mReviewCount; + private TextView mPrice; + private OnLongClickListener mOnLongClickListener; + + public AppDiscoveryItemView(Context context) { + this(context, null); + } + + public AppDiscoveryItemView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppDiscoveryItemView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + this.mImage = (ImageView) findViewById(R.id.image); + this.mBadge = (ImageView) findViewById(R.id.badge); + this.mTitle = (TextView) findViewById(R.id.title); + this.mRatingText = (TextView) findViewById(R.id.rating); + this.mRatingView = (RatingView) findViewById(R.id.rating_view); + this.mPrice = (TextView) findViewById(R.id.price); + this.mReviewCount = (TextView) findViewById(R.id.review_count); + } + + public void init(OnClickListener clickListener, + AccessibilityDelegate accessibilityDelegate, + OnLongClickListener onLongClickListener) { + setOnClickListener(clickListener); + mImage.setOnClickListener(clickListener); + setAccessibilityDelegate(accessibilityDelegate); + mOnLongClickListener = onLongClickListener; + } + + public void apply(@NonNull AppDiscoveryAppInfo info) { + setTag(info); + mImage.setTag(info); + mImage.setImageBitmap(info.iconBitmap); + mImage.setOnLongClickListener(info.isDragAndDropSupported() ? mOnLongClickListener : null); + mBadge.setVisibility(info.isInstantApp ? View.VISIBLE : View.GONE); + mTitle.setText(info.title); + mPrice.setText(info.priceFormatted != null ? info.priceFormatted : ""); + mReviewCount.setVisibility(SHOW_REVIEW_COUNT ? View.VISIBLE : View.GONE); + if (info.rating >= 0) { + mRatingText.setText(new DecimalFormat("#.#").format(info.rating)); + mRatingView.setRating(info.rating); + mRatingView.setVisibility(View.VISIBLE); + String reviewCountFormatted = NumberFormat.getInstance().format(info.reviewCount); + mReviewCount.setText("(" + reviewCountFormatted + ")"); + } else { + // if we don't have a rating + mRatingView.setVisibility(View.GONE); + mRatingText.setText(""); + mReviewCount.setText(""); + } + } +} diff --git a/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java b/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java new file mode 100644 index 000000000..0700a1023 --- /dev/null +++ b/src/com/android/launcher3/discovery/AppDiscoveryUpdateState.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2017 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.discovery; + +public enum AppDiscoveryUpdateState { + START, UPDATE, END +} diff --git a/src/com/android/launcher3/discovery/RatingView.java b/src/com/android/launcher3/discovery/RatingView.java new file mode 100644 index 000000000..8fe63d6ba --- /dev/null +++ b/src/com/android/launcher3/discovery/RatingView.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.discovery; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.ClipDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; + +import com.android.launcher3.R; + +/** + * A simple rating view that shows stars with a rating from 0-5. + */ +public class RatingView extends View { + + private static final float WIDTH_FACTOR = 0.9f; + private static final int MAX_LEVEL = 10000; + private static final int MAX_STARS = 5; + + private final Drawable mStarDrawable; + private final int mColorGray; + private final int mColorHighlight; + + private float rating; + + public RatingView(Context context) { + this(context, null); + } + + public RatingView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RatingView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mStarDrawable = getResources().getDrawable(R.drawable.ic_star_rating, null); + mColorGray = 0x1E000000; + mColorHighlight = 0x8A000000; + } + + public void setRating(float rating) { + this.rating = Math.min(Math.max(rating, 0), MAX_STARS); + } + + @Override + protected void onDraw(Canvas canvas) { + drawStars(canvas, MAX_STARS, mColorGray); + drawStars(canvas, rating, mColorHighlight); + } + + private void drawStars(Canvas canvas, float stars, int color) { + int fullWidth = getLayoutParams().width; + int cellWidth = fullWidth / MAX_STARS; + int starWidth = (int) (cellWidth * WIDTH_FACTOR); + int padding = cellWidth - starWidth; + int fullStars = (int) stars; + float partialStarFactor = stars - fullStars; + + for (int i = 0; i < fullStars; i++) { + int x = i * cellWidth + padding; + Drawable star = mStarDrawable.getConstantState().newDrawable().mutate(); + star.setTint(color); + star.setBounds(x, padding, x + starWidth, padding + starWidth); + star.draw(canvas); + } + if (partialStarFactor > 0f) { + int x = fullStars * cellWidth + padding; + ClipDrawable star = new ClipDrawable(mStarDrawable, + Gravity.LEFT, ClipDrawable.HORIZONTAL); + star.setTint(color); + star.setLevel((int) (MAX_LEVEL * partialStarFactor)); + star.setBounds(x, padding, x + starWidth, padding + starWidth); + star.draw(canvas); + } + } +} diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java index e4c9be4db..7806c98be 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -24,7 +24,6 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; @@ -36,6 +35,7 @@ import android.view.animation.DecelerateInterpolator; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; +import com.android.launcher3.util.Themes; import com.android.launcher3.util.Thunk; import java.util.Arrays; @@ -259,7 +259,7 @@ public class DragView extends View { m1.setSaturation(0); ColorMatrix m2 = new ColorMatrix(); - setColorScale(color, m2); + Themes.setColorScaleOnMatrix(color, m2); m1.postConcat(m2); animateFilterTo(m1.getArray()); @@ -384,11 +384,6 @@ public class DragView extends View { } } - public static void setColorScale(int color, ColorMatrix target) { - target.setScale(Color.red(color) / 255f, Color.green(color) / 255f, - Color.blue(color) / 255f, Color.alpha(color) / 255f); - } - public int getBlurSizeOutline() { return mBlurSizeOutline; } diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java index 840fcf5fe..503c2ec9f 100644 --- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java +++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java @@ -121,6 +121,11 @@ public class ClippedFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule } @Override + public float getIconSize() { + return mIconSize; + } + + @Override public int maxNumItems() { return MAX_NUM_ITEMS_IN_PREVIEW; } @@ -129,24 +134,4 @@ public class ClippedFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule public boolean clipToBackground() { return true; } - - @Override - public List<View> getItemsToDisplay(Folder folder) { - List<View> items = new ArrayList<>(folder.getItemsInReadingOrder()); - int numItems = items.size(); - if (FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION && numItems > MAX_NUM_ITEMS_IN_PREVIEW) { - // We match the icons in the preview with the layout of the opened folder (b/27944225), - // but we still need to figure out how we want to handle updating the preview when the - // upper left quadrant changes. - int appsPerRow = folder.mContent.getPageAt(0).getCountX(); - int appsToDelete = appsPerRow - MAX_NUM_ITEMS_PER_ROW; - - // We only display the upper left quadrant. - while (appsToDelete > 0) { - items.remove(MAX_NUM_ITEMS_PER_ROW); - appsToDelete--; - } - } - return items.subList(0, Math.min(numItems, MAX_NUM_ITEMS_IN_PREVIEW)); - } } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 3d28f2291..15bdea986 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -46,6 +46,7 @@ import android.widget.TextView; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Alarm; import com.android.launcher3.AppInfo; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; @@ -56,7 +57,6 @@ import com.android.launcher3.FolderInfo.FolderListener; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; -import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LogDecelerateInterpolator; import com.android.launcher3.OnAlarmListener; @@ -67,22 +67,22 @@ import com.android.launcher3.UninstallDropTarget.DropTargetSource; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.accessibility.AccessibleDragListenerAdapter; +import com.android.launcher3.anim.AnimationLayerSet; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragController.DragListener; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.pageindicators.PageIndicatorDots; -import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; -import com.android.launcher3.util.CircleRevealOutlineProvider; +import com.android.launcher3.anim.CircleRevealOutlineProvider; import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.List; /** * Represents a set of icons chosen by the user or generated by the system. @@ -135,8 +135,10 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + private FolderAnimationManager mFolderAnimationManager; + private final int mExpandDuration; - private final int mMaterialExpandDuration; + public final int mMaterialExpandDuration; private final int mMaterialExpandStagger; protected final Launcher mLauncher; @@ -249,8 +251,11 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC } mFolderName.setOnEditorActionListener(this); mFolderName.setSelectAllOnFocus(true); - mFolderName.setInputType(mFolderName.getInputType() | - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + mFolderName.setInputType(mFolderName.getInputType() + & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT + & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + mFolderName.forceDisableSuggestions(true); mFooter = findViewById(R.id.folder_footer); @@ -474,6 +479,8 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC } } }); + + mFolderAnimationManager = new FolderAnimationManager(this); } /** @@ -509,51 +516,13 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mState = STATE_SMALL; } - /** - * Opens the user folder described by the specified tag. The opening of the folder - * is animated relative to the specified View. If the View is null, no animation - * is played. - */ - public void animateOpen() { - Folder openFolder = getOpen(mLauncher); - if (openFolder != null && openFolder != this) { - // Close any open folder before opening a folder. - openFolder.close(true); - } - - DragLayer dragLayer = mLauncher.getDragLayer(); - // Just verify that the folder hasn't already been added to the DragLayer. - // There was a one-off crash where the folder had a parent already. - if (getParent() == null) { - dragLayer.addView(this); - mDragController.addDropTarget(this); - } else { - if (ProviderConfig.IS_DOGFOOD_BUILD) { - Log.e(TAG, "Opening folder (" + this + ") which already has a parent:" - + getParent()); - } - } - - mIsOpen = true; - mFolderIcon.growAndFadeOut(); - - mContent.completePendingPageChanges(); - if (!mDragInProgress) { - // Open on the first page. - mContent.snapToPageImmediately(0); - } - - // This is set to true in close(), but isn't reset to false until onDropCompleted(). This - // leads to an inconsistent state if you drag out of the folder and drag back in without - // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. - mDeleteFolderOnDropCompleted = false; - - final Runnable onCompleteRunnable; + private AnimatorSet getOpeningAnimatorSet() { prepareReveal(); - centerAboutIcon(); + mFolderIcon.growAndFadeOut(); AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + + int width = getFolderWidth(); int height = getFolderHeight(); float transX = - 0.075f * (width / 2 - getPivotX()); @@ -594,13 +563,61 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC anim.play(textAlpha); anim.play(reveal); - mContent.setLayerType(LAYER_TYPE_HARDWARE, null); - mFooter.setLayerType(LAYER_TYPE_HARDWARE, null); + AnimationLayerSet layerSet = new AnimationLayerSet(); + layerSet.addView(mContent); + layerSet.addView(mFooter); + anim.addListener(layerSet); + + return anim; + } + + /** + * Opens the user folder described by the specified tag. The opening of the folder + * is animated relative to the specified View. If the View is null, no animation + * is played. + */ + public void animateOpen() { + Folder openFolder = getOpen(mLauncher); + if (openFolder != null && openFolder != this) { + // Close any open folder before opening a folder. + openFolder.close(true); + } + + DragLayer dragLayer = mLauncher.getDragLayer(); + // Just verify that the folder hasn't already been added to the DragLayer. + // There was a one-off crash where the folder had a parent already. + if (getParent() == null) { + dragLayer.addView(this); + mDragController.addDropTarget(this); + } else { + if (FeatureFlags.IS_DOGFOOD_BUILD) { + Log.e(TAG, "Opening folder (" + this + ") which already has a parent:" + + getParent()); + } + } + + mIsOpen = true; + + mContent.completePendingPageChanges(); + if (!mDragInProgress) { + // Open on the first page. + mContent.snapToPageImmediately(0); + } + + // This is set to true in close(), but isn't reset to false until onDropCompleted(). This + // leads to an inconsistent state if you drag out of the folder and drag back in without + // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. + mDeleteFolderOnDropCompleted = false; + + final Runnable onCompleteRunnable; + centerAboutIcon(); + + AnimatorSet anim = FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION + ? mFolderAnimationManager.getOpeningAnimator() + : getOpeningAnimatorSet(); onCompleteRunnable = new Runnable() { @Override public void run() { - mContent.setLayerType(LAYER_TYPE_NONE, null); - mFooter.setLayerType(LAYER_TYPE_NONE, null); mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); } }; @@ -695,7 +712,7 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mFolderName.dispatchBackKey(); } - if (mFolderIcon != null) { + if (mFolderIcon != null && !FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION) { mFolderIcon.shrinkAndFadeIn(animate); } @@ -713,12 +730,24 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC parent.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } + private AnimatorSet getClosingAnimatorSet() { + AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet(); + animatorSet.play(LauncherAnimUtils.ofViewAlphaAndScale(this, 0, 0.9f, 0.9f)); + + AnimationLayerSet layerSet = new AnimationLayerSet(); + layerSet.addView(this); + animatorSet.addListener(layerSet); + animatorSet.setDuration(mExpandDuration); + return animatorSet; + } + private void animateClosed() { - final ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(this, 0, 0.9f, 0.9f); - oa.addListener(new AnimatorListenerAdapter() { + AnimatorSet a = FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION + ? mFolderAnimationManager.getClosingAnimator() + : getClosingAnimatorSet(); + a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - setLayerType(LAYER_TYPE_NONE, null); closeComplete(true); } @Override @@ -730,9 +759,7 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mState = STATE_ANIMATING; } }); - oa.setDuration(mExpandDuration); - setLayerType(LAYER_TYPE_HARDWARE, null); - oa.start(); + a.start(); } private void closeComplete(boolean wasAnimated) { @@ -743,10 +770,14 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC } mDragController.removeDropTarget(this); clearFocus(); - if (wasAnimated) { - mFolderIcon.requestFocus(); + if (mFolderIcon != null) { + mFolderIcon.setVisibility(View.VISIBLE); + if (wasAnimated) { + mFolderIcon.requestFocus(); + } } + if (mRearrangeOnClose) { rearrangeChildren(); mRearrangeOnClose = false; @@ -1046,7 +1077,7 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); - int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int width = getFolderWidth(); int height = getFolderHeight(); float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); @@ -1088,6 +1119,7 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC int folderPivotY = height / 2 + (centeredTop - top); setPivotX(folderPivotX); setPivotY(folderPivotY); + mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * (1.0f * folderPivotX / width)); mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * @@ -1119,6 +1151,10 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN); } + private int getFolderWidth() { + return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + } + private int getFolderHeight() { return getFolderHeight(getContentAreaHeight()); } @@ -1405,6 +1441,11 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC updateTextViewFocus(); } + @Override + public void prepareAutoUpdate() { + close(false); + } + public void onTitleChanged(CharSequence title) { } @@ -1424,6 +1465,26 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC return mItemsInReadingOrder; } + public List<BubbleTextView> getItemsOnCurrentPage() { + ArrayList<View> allItems = getItemsInReadingOrder(); + int currentPage = mContent.getCurrentPage(); + int lastPage = mContent.getPageCount() - 1; + int totalItemsInFolder = allItems.size(); + int itemsPerPage = mContent.itemsPerPage(); + int numItemsOnCurrentPage = currentPage == lastPage + ? totalItemsInFolder - (itemsPerPage * currentPage) + : itemsPerPage; + + int startIndex = currentPage * itemsPerPage; + int endIndex = startIndex + numItemsOnCurrentPage; + + List<BubbleTextView> itemsOnCurrentPage = new ArrayList<>(numItemsOnCurrentPage); + for (int i = startIndex; i < endIndex; ++i) { + itemsOnCurrentPage.add((BubbleTextView) allItems.get(i)); + } + return itemsOnCurrentPage; + } + public void onFocusChange(View v, boolean hasFocus) { if (v == mFolderName) { if (hasFocus) { diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java new file mode 100644 index 000000000..6ce572d94 --- /dev/null +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2017 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.folder; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.support.v4.graphics.ColorUtils; +import android.util.Property; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.ShortcutAndWidgetContainer; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.util.Themes; + +import java.util.List; + +/** + * Manages the opening and closing animations for a {@link Folder}. + * + * All of the animations are done in the Folder. + * ie. When the user taps on the FolderIcon, we immediately hide the FolderIcon and show the Folder + * in its place before starting the animation. + */ +public class FolderAnimationManager { + + private Folder mFolder; + private FolderPagedView mContent; + private GradientDrawable mFolderBackground; + + private FolderIcon mFolderIcon; + private FolderIcon.PreviewBackground mPreviewBackground; + + private Context mContext; + private Launcher mLauncher; + + private Animator mRevealAnimator; + private final TimeInterpolator mOpeningInterpolator; + private final TimeInterpolator mClosingInterpolator; + private final TimeInterpolator mPreviewItemOpeningInterpolator; + private final TimeInterpolator mPreviewItemClosingInterpolator; + + private final FolderIcon.PreviewItemDrawingParams mTmpParams = + new FolderIcon.PreviewItemDrawingParams(0, 0, 0, 0); + + private static final Property<View, Float> SCALE_PROPERTY = + new Property<View, Float>(Float.class, "scale") { + @Override + public Float get(View view) { + return view.getScaleX(); + } + + @Override + public void set(View view, Float scale) { + view.setScaleX(scale); + view.setScaleY(scale); + } + }; + + private static final Property<List<BubbleTextView>, Integer> ITEMS_TEXT_COLOR_PROPERTY = + new Property<List<BubbleTextView>, Integer>(Integer.class, "textColor") { + @Override + public Integer get(List<BubbleTextView> items) { + return items.get(0).getCurrentTextColor(); + } + + @Override + public void set(List<BubbleTextView> items, Integer color) { + int size = items.size(); + + for (int i = 0; i < size; ++i) { + items.get(i).setTextColor(color); + } + } + }; + + public FolderAnimationManager(Folder folder) { + mFolder = folder; + mContent = folder.mContent; + mFolderBackground = (GradientDrawable) mFolder.getBackground(); + + mFolderIcon = folder.mFolderIcon; + mPreviewBackground = mFolderIcon.mBackground; + + mContext = folder.getContext(); + mLauncher = folder.mLauncher; + + mOpeningInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_opening_interpolator); + mClosingInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_closing_interpolator); + mPreviewItemOpeningInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_preview_item_opening_interpolator); + mPreviewItemClosingInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_preview_item_closing_interpolator); + } + + public AnimatorSet getOpeningAnimator() { + mFolder.setPivotX(0); + mFolder.setPivotY(0); + + AnimatorSet a = getAnimatorSet(true /* isOpening */); + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mFolderIcon.setVisibility(View.INVISIBLE); + } + }); + return a; + } + + public AnimatorSet getClosingAnimator() { + AnimatorSet a = getAnimatorSet(false /* isOpening */); + return a; + } + + /** + * Prepares the Folder for animating between open / closed states. + * + * @param isOpening If true, return the animator set for the opening animation. + */ + private AnimatorSet getAnimatorSet(final boolean isOpening) { + final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams(); + FolderIcon.PreviewLayoutRule rule = mFolderIcon.getLayoutRule(); + final List<BubbleTextView> itemsInPreview = mFolderIcon.getItemsToDisplay(); + + // Match size/scale of icons in the preview + float previewScale = rule.scaleForItem(0, itemsInPreview.size()); + float previewSize = rule.getIconSize() * previewScale; + float folderScale = previewSize / itemsInPreview.get(0).getIconSize(); + + final float initialScale = folderScale; + final float finalScale = 1f; + float scale = isOpening ? initialScale : finalScale; + mFolder.setScaleX(scale); + mFolder.setScaleY(scale); + + // Match position of the FolderIcon + final Rect folderIconPos = new Rect(); + float scaleRelativeToDragLayer = mLauncher.getDragLayer() + .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); + folderScale *= scaleRelativeToDragLayer; + + // We want to create a small X offset for the preview items, so that they follow their + // expected path to their final locations. ie. an icon should not move right, if it's final + // location is to its left. This value is arbitrarily defined. + final int nudgeOffsetX = (int) (previewSize / 2); + + final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft()) + * folderScale); + final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop()) + * folderScale); + + int initialX = folderIconPos.left + mFolderIcon.mBackground.getOffsetX() - paddingOffsetX + - nudgeOffsetX; + int initialY = folderIconPos.top + mFolderIcon.mBackground.getOffsetY() - paddingOffsetY; + final float xDistance = initialX - lp.x; + final float yDistance = initialY - lp.y; + + // Set up the Folder background. + final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary); + final int initialColor = + ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha()); + mFolderBackground.setColor(isOpening ? initialColor : finalColor); + + // Initialize the Folder items' text. + final List<BubbleTextView> itemsOnCurrentPage = mFolder.getItemsOnCurrentPage(); + final int finalTextColor = Themes.getAttrColor(mContext, android.R.attr.textColorSecondary); + ITEMS_TEXT_COLOR_PROPERTY.set(itemsOnCurrentPage, isOpening ? Color.TRANSPARENT + : finalTextColor); + + // Create the animators. + AnimatorSet a = LauncherAnimUtils.createAnimatorSet(); + a.setDuration(mFolder.mMaterialExpandDuration); + + ObjectAnimator translationX = isOpening + ? ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_X, xDistance, 0) + : ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_X, 0, xDistance); + a.play(translationX); + + ObjectAnimator translationY = isOpening + ? ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_Y, yDistance, 0) + : ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_Y, 0, yDistance); + a.play(translationY); + + ObjectAnimator scaleAnimator = isOpening + ? ObjectAnimator.ofFloat(mFolder, SCALE_PROPERTY, initialScale, finalScale) + : ObjectAnimator.ofFloat(mFolder, SCALE_PROPERTY, finalScale, initialScale); + a.play(scaleAnimator); + + ObjectAnimator itemsTextColor = isOpening + ? ObjectAnimator.ofArgb(itemsOnCurrentPage, ITEMS_TEXT_COLOR_PROPERTY, + Color.TRANSPARENT, finalTextColor) + : ObjectAnimator.ofArgb(itemsOnCurrentPage, ITEMS_TEXT_COLOR_PROPERTY, + finalTextColor, Color.TRANSPARENT); + a.play(itemsTextColor); + + ObjectAnimator backgroundColor = isOpening + ? ObjectAnimator.ofArgb(mFolderBackground, "color", initialColor, finalColor) + : ObjectAnimator.ofArgb(mFolderBackground, "color", finalColor, initialColor); + a.play(backgroundColor); + + // Set up the reveal animation that clips the Folder. + float stroke = mPreviewBackground.getStrokeWidth(); + int initialSize = (int) ((mFolderIcon.mBackground.getRadius() * 2 + stroke) / folderScale); + int totalOffsetX = paddingOffsetX + Math.round(nudgeOffsetX / folderScale); + int unscaledStroke = (int) Math.floor(stroke / folderScale); + Rect startRect = new Rect(totalOffsetX + unscaledStroke, unscaledStroke, + totalOffsetX + initialSize, initialSize); + Rect endRect = new Rect(0, 0, lp.width, lp.height); + a.play(getRevealAnimator(isOpening, initialSize / 2f, startRect, endRect)); + + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + ITEMS_TEXT_COLOR_PROPERTY.set(itemsOnCurrentPage, finalTextColor); + } + }); + + // We set the interpolator on all current child animators here, because the preview item + // animators may use a different interpolator. + for (Animator animator : a.getChildAnimations()) { + animator.setInterpolator(isOpening ? mOpeningInterpolator : mClosingInterpolator); + } + + addPreviewItemAnimatorsToSet(a, isOpening, folderScale, nudgeOffsetX); + return a; + } + + private Animator getRevealAnimator(boolean isOpening, float circleRadius, Rect start, + Rect end) { + boolean revealIsRunning = mRevealAnimator != null && mRevealAnimator.isRunning(); + final float finalRadius = revealIsRunning + ? ((RoundedRectRevealOutlineProvider) mFolder.getOutlineProvider()).getRadius() + : Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics()); + if (revealIsRunning) { + mRevealAnimator.cancel(); + } + mRevealAnimator = new RoundedRectRevealOutlineProvider(circleRadius, finalRadius, + start, end).createRevealAnimator(mFolder, !isOpening); + return mRevealAnimator; + } + + /** + * Animate the items that are displayed in the preview. + */ + private void addPreviewItemAnimatorsToSet(AnimatorSet animatorSet, boolean isOpening, + final float folderScale, int nudgeOffsetX) { + FolderIcon.PreviewLayoutRule rule = mFolderIcon.getLayoutRule(); + final List<BubbleTextView> itemsInPreview = mFolderIcon.getItemsToDisplay(); + final int numItemsInPreview = itemsInPreview.size(); + + TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator(isOpening); + + ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets(); + for (int i = 0; i < numItemsInPreview; ++i) { + final BubbleTextView btv = itemsInPreview.get(i); + CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams(); + + // Calculate the final values in the LayoutParams. + btvLp.isLockedToGrid = true; + cwc.setupLp(btv); + + // Match scale of icons in the preview. + float previewScale = rule.scaleForItem(i, numItemsInPreview); + float previewSize = rule.getIconSize() * previewScale; + float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); + + final float initialScale = iconScale / folderScale; + final float finalScale = 1f; + float scale = isOpening ? initialScale : finalScale; + btv.setScaleX(scale); + btv.setScaleY(scale); + + // Match positions of the icons in the folder with their positions in the preview + rule.computePreviewItemDrawingParams(i, numItemsInPreview, mTmpParams); + // The PreviewLayoutRule assumes that the icon size takes up the entire width so we + // offset by the actual size. + int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2; + + final int previewPosX = + (int) ((mTmpParams.transX - iconOffsetX + nudgeOffsetX) / folderScale); + final int previewPosY = (int) (mTmpParams.transY / folderScale); + + final float xDistance = previewPosX - btvLp.x; + final float yDistance = previewPosY - btvLp.y; + + ObjectAnimator translationX = isOpening + ? ObjectAnimator.ofFloat(btv, View.TRANSLATION_X, xDistance, 0) + : ObjectAnimator.ofFloat(btv, View.TRANSLATION_X, 0, xDistance); + translationX.setInterpolator(previewItemInterpolator); + animatorSet.play(translationX); + + ObjectAnimator translationY = isOpening + ? ObjectAnimator.ofFloat(btv, View.TRANSLATION_Y, yDistance, 0) + : ObjectAnimator.ofFloat(btv, View.TRANSLATION_Y, 0, yDistance); + translationY.setInterpolator(previewItemInterpolator); + animatorSet.play(translationY); + + ObjectAnimator scaleAnimator = isOpening + ? ObjectAnimator.ofFloat(btv, SCALE_PROPERTY, initialScale, finalScale) + : ObjectAnimator.ofFloat(btv, SCALE_PROPERTY, finalScale, initialScale); + scaleAnimator.setInterpolator(previewItemInterpolator); + animatorSet.play(scaleAnimator); + + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + btv.setTranslationX(0.0f); + btv.setTranslationY(0.0f); + btv.setScaleX(1f); + btv.setScaleY(1f); + } + }); + } + } + + private TimeInterpolator getPreviewItemInterpolator(boolean isOpening) { + if (mFolder.getItemCount() > FolderIcon.NUM_ITEMS_IN_PREVIEW) { + // With larger folders, we want the preview items to reach their final positions faster + // (when opening) and later (when closing) so that they appear aligned with the rest of + // the folder items when they are both visible. + return isOpening ? mPreviewItemOpeningInterpolator : mPreviewItemClosingInterpolator; + } + return isOpening ? mOpeningInterpolator : mClosingInterpolator; + } +} diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 604540022..ced9c9e8d 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -24,11 +24,15 @@ import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Parcelable; import android.util.AttributeSet; @@ -121,12 +125,11 @@ public class FolderIcon extends FrameLayout implements FolderListener { private float mSlop; + FolderIconPreviewVerifier mPreviewVerifier; private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0); private ArrayList<PreviewItemDrawingParams> mDrawingParams = new ArrayList<PreviewItemDrawingParams>(); private Drawable mReferenceDrawable = null; - Paint mBgPaint = new Paint(); - private Alarm mOpenAlarm = new Alarm(); private FolderBadgeInfo mBadgeInfo = new FolderBadgeInfo(); @@ -180,11 +183,6 @@ public class FolderIcon extends FrameLayout implements FolderListener { FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext()) .inflate(resId, group, false); - // For performance and compatibility reasons we render the preview using a software layer. - // In particular, hardware path clipping has spotty ecosystem support and bad performance. - // Software rendering also allows us to use shadow layers. - icon.setLayerType(LAYER_TYPE_SOFTWARE, new Paint(Paint.FILTER_BITMAP_FLAG)); - icon.setClipToPadding(false); icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); icon.mFolderName.setText(folderInfo.title); @@ -223,6 +221,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { private void setFolder(Folder folder) { mFolder = folder; + mPreviewVerifier = new FolderIconPreviewVerifier(mLauncher.getDeviceProfile().inv); updateItemDrawingParams(false); } @@ -409,6 +408,10 @@ public class FolderIcon extends FrameLayout implements FolderListener { mBadgeInfo = badgeInfo; } + public PreviewLayoutRule getLayoutRule() { + return mPreviewLayoutRule; + } + /** * Sets mBadgeScale to 1 or 0, animating if oldCount or newCount is 0 * (the badge is being added or removed). @@ -494,7 +497,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { - canvas.save(); + canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(params.transX, params.transY); canvas.scale(params.scale, params.scale); Drawable d = params.drawable; @@ -521,10 +524,29 @@ public class FolderIcon extends FrameLayout implements FolderListener { * information, handles drawing, and animation (accept state <--> rest state). */ public static class PreviewBackground { + + private final PorterDuffXfermode mClipPorterDuffXfermode + = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); + // Create a RadialGradient such that it draws a black circle and then extends with + // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and + // just at the edge quickly change it to transparent. + private final RadialGradient mClipShader = new RadialGradient(0, 0, 1, + new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT }, + new float[] {0, 0.999f, 1}, + Shader.TileMode.CLAMP); + + private final PorterDuffXfermode mShadowPorterDuffXfermode + = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); + private RadialGradient mShadowShader = null; + + private final Matrix mShaderMatrix = new Matrix(); + private final Path mPath = new Path(); + + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private float mScale = 1f; private float mColorMultiplier = 1f; - private Path mClipPath = new Path(); - private int mStrokeWidth; + private float mStrokeWidth; private View mInvalidateDelegate; public int previewSize; @@ -547,7 +569,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { private static final int BG_OPACITY = 160; private static final int MAX_BG_OPACITY = 225; private static final int BG_INTENSITY = 245; - private static final int SHADOW_OPACITY = 80; + private static final int SHADOW_OPACITY = 40; ValueAnimator mScaleAnimator; @@ -563,7 +585,16 @@ public class FolderIcon extends FrameLayout implements FolderListener { basePreviewOffsetX = (availableSpace - this.previewSize) / 2; basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding; - mStrokeWidth = Utilities.pxFromDp(1, dm); + // Stroke width is 1dp + mStrokeWidth = dm.density; + + float radius = getScaledRadius(); + float shadowRadius = radius + mStrokeWidth; + int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0); + mShadowShader = new RadialGradient(0, 0, 1, + new int[] {shadowColor, Color.TRANSPARENT}, + new float[] {radius / shadowRadius, 1}, + Shader.TileMode.CLAMP); invalidate(); } @@ -593,10 +624,6 @@ public class FolderIcon extends FrameLayout implements FolderListener { } void invalidate() { - int radius = getScaledRadius(); - mClipPath.reset(); - mClipPath.addCircle(radius, radius, radius, Path.Direction.CW); - if (mInvalidateDelegate != null) { mInvalidateDelegate.invalidate(); } @@ -611,70 +638,94 @@ public class FolderIcon extends FrameLayout implements FolderListener { invalidate(); } - public void drawBackground(Canvas canvas, Paint paint) { - canvas.save(); - canvas.translate(getOffsetX(), getOffsetY()); - - paint.reset(); - paint.setStyle(Paint.Style.FILL); - paint.setXfermode(null); - paint.setAntiAlias(true); - + public void drawBackground(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier); - paint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); + mPaint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); + + drawCircle(canvas, 0 /* deltaRadius */); + // Draw shadow. + if (mShadowShader == null) { + return; + } float radius = getScaledRadius(); + float shadowRadius = radius + mStrokeWidth; + mPaint.setColor(Color.BLACK); + int offsetX = getOffsetX(); + int offsetY = getOffsetY(); + final int saveCount; + if (canvas.isHardwareAccelerated()) { + saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY, + offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, + null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG); - canvas.drawCircle(radius, radius, radius, paint); - canvas.clipPath(mClipPath, Region.Op.DIFFERENCE); + } else { + saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + clipCanvasSoftware(canvas, Region.Op.DIFFERENCE); + } - paint.setStyle(Paint.Style.STROKE); - paint.setColor(Color.TRANSPARENT); - paint.setShadowLayer(mStrokeWidth, 0, mStrokeWidth, Color.argb(SHADOW_OPACITY, 0, 0, 0)); - canvas.drawCircle(radius, radius, radius, paint); + mShaderMatrix.setScale(shadowRadius, shadowRadius); + mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY); + mShadowShader.setLocalMatrix(mShaderMatrix); + mPaint.setShader(mShadowShader); + canvas.drawPaint(mPaint); + mPaint.setShader(null); + + if (canvas.isHardwareAccelerated()) { + mPaint.setXfermode(mShadowPorterDuffXfermode); + canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint); + mPaint.setXfermode(null); + } - canvas.restore(); + canvas.restoreToCount(saveCount); } - public void drawBackgroundStroke(Canvas canvas, Paint paint) { - canvas.save(); - canvas.translate(getOffsetX(), getOffsetY()); - - paint.reset(); - paint.setAntiAlias(true); - paint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(mStrokeWidth); - - float radius = getScaledRadius(); - canvas.drawCircle(radius, radius, radius - 1, paint); - - canvas.restore(); + public void drawBackgroundStroke(Canvas canvas) { + mPaint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mStrokeWidth); + drawCircle(canvas, 1 /* deltaRadius */); } - public void drawLeaveBehind(Canvas canvas, Paint paint) { + public void drawLeaveBehind(Canvas canvas) { float originalScale = mScale; mScale = 0.5f; - canvas.save(); - canvas.translate(getOffsetX(), getOffsetY()); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(Color.argb(160, 245, 245, 245)); + drawCircle(canvas, 0 /* deltaRadius */); - paint.reset(); - paint.setAntiAlias(true); - paint.setColor(Color.argb(160, 245, 245, 245)); + mScale = originalScale; + } + private void drawCircle(Canvas canvas,float deltaRadius) { float radius = getScaledRadius(); - canvas.drawCircle(radius, radius, radius, paint); + canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(), + radius - deltaRadius, mPaint); + } - canvas.restore(); - mScale = originalScale; + // It is the callers responsibility to save and restore the canvas layers. + private void clipCanvasSoftware(Canvas canvas, Region.Op op) { + mPath.reset(); + float r = getScaledRadius(); + mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW); + canvas.clipPath(mPath, op); } - // It is the callers responsibility to save and restore the canvas. - private void clipCanvas(Canvas canvas) { - canvas.translate(getOffsetX(), getOffsetY()); - canvas.clipPath(mClipPath); - canvas.translate(-getOffsetX(), -getOffsetY()); + // It is the callers responsibility to save and restore the canvas layers. + private void clipCanvasHardware(Canvas canvas) { + mPaint.setColor(Color.BLACK); + mPaint.setXfermode(mClipPorterDuffXfermode); + + float radius = getScaledRadius(); + mShaderMatrix.setScale(radius, radius); + mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY()); + mClipShader.setLocalMatrix(mShaderMatrix); + mPaint.setShader(mClipShader); + canvas.drawPaint(mPaint); + mPaint.setXfermode(null); + mPaint.setShader(null); } private void delegateDrawing(CellLayout delegate, int cellX, int cellY) { @@ -778,6 +829,14 @@ public class FolderIcon extends FrameLayout implements FolderListener { }; animateScale(1f, 1f, onStart, onEnd); } + + public int getBackgroundAlpha() { + return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier); + } + + public float getStrokeWidth() { + return mStrokeWidth; + } } public void setFolderBackground(PreviewBackground bg) { @@ -794,17 +853,22 @@ public class FolderIcon extends FrameLayout implements FolderListener { } if (!mBackground.drawingDelegated()) { - mBackground.drawBackground(canvas, mBgPaint); + mBackground.drawBackground(canvas); } if (mFolder == null) return; if (mFolder.getItemCount() == 0 && !mAnimating) return; - canvas.save(); + final int saveCount; - - if (mPreviewLayoutRule.clipToBackground()) { - mBackground.clipCanvas(canvas); + if (canvas.isHardwareAccelerated()) { + saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, + Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); + } else { + saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + if (mPreviewLayoutRule.clipToBackground()) { + mBackground.clipCanvasSoftware(canvas, Region.Op.INTERSECT); + } } // The items are drawn in coordinates relative to the preview offset @@ -817,18 +881,24 @@ public class FolderIcon extends FrameLayout implements FolderListener { drawPreviewItem(canvas, p); } } - canvas.restore(); + canvas.translate(-mBackground.basePreviewOffsetX, -mBackground.basePreviewOffsetY); + + if (mPreviewLayoutRule.clipToBackground() && canvas.isHardwareAccelerated()) { + mBackground.clipCanvasHardware(canvas); + } + canvas.restoreToCount(saveCount); if (mPreviewLayoutRule.clipToBackground() && !mBackground.drawingDelegated()) { - mBackground.drawBackgroundStroke(canvas, mBgPaint); + mBackground.drawBackgroundStroke(canvas); } - int offsetX = mBackground.getOffsetX(); - int offsetY = mBackground.getOffsetY(); - int previewSize = (int) (mBackground.previewSize * mBackground.mScale); - Rect bounds = new Rect(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize); if ((mBadgeInfo != null && mBadgeInfo.getNotificationCount() > 0) || mBadgeScale > 0) { // If we are animating to the accepting state, animate the badge out. + int offsetX = mBackground.getOffsetX(); + int offsetY = mBackground.getOffsetY(); + int previewSize = (int) (mBackground.previewSize * mBackground.mScale); + Rect bounds = new Rect(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize); + float badgeScale = Math.max(0, mBadgeScale - mBackground.getScaleProgress()); mBadgeRenderer.draw(canvas, IconPalette.FOLDER_ICON_PALETTE, mBadgeInfo, bounds, badgeScale); } @@ -934,8 +1004,26 @@ public class FolderIcon extends FrameLayout implements FolderListener { return mFolderName.getVisibility() == VISIBLE; } + public List<BubbleTextView> getItemsToDisplay() { + mPreviewVerifier.setFolderInfo(mFolder.getInfo()); + + List<BubbleTextView> itemsToDisplay = new ArrayList<>(); + List<View> allItems = mFolder.getItemsInReadingOrder(); + int numItems = allItems.size(); + for (int rank = 0; rank < numItems; ++rank) { + if (mPreviewVerifier.isItemInPreview(rank)) { + itemsToDisplay.add((BubbleTextView) allItems.get(rank)); + } + + if (itemsToDisplay.size() == FolderIcon.NUM_ITEMS_IN_PREVIEW) { + break; + } + } + return itemsToDisplay; + } + private void updateItemDrawingParams(boolean animate) { - List<View> items = mPreviewLayoutRule.getItemsToDisplay(mFolder); + List<BubbleTextView> items = getItemsToDisplay(); int nItemsInPreview = items.size(); int prevNumItems = mDrawingParams.size(); @@ -950,7 +1038,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { for (int i = 0; i < mDrawingParams.size(); i++) { PreviewItemDrawingParams p = mDrawingParams.get(i); - p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1]; + p.drawable = items.get(i).getCompoundDrawables()[1]; if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) { computePreviewItemDrawingParams(i, nItemsInPreview, p); @@ -982,6 +1070,10 @@ public class FolderIcon extends FrameLayout implements FolderListener { } @Override + public void prepareAutoUpdate() { + } + + @Override public void onAdd(ShortcutInfo item) { int oldCount = mBadgeInfo.getNotificationCount(); mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item)); @@ -1115,8 +1207,8 @@ public class FolderIcon extends FrameLayout implements FolderListener { PreviewItemDrawingParams params); void init(int availableSpace, int intrinsicIconSize, boolean rtl); float scaleForItem(int index, int totalNumItems); + float getIconSize(); int maxNumItems(); boolean clipToBackground(); - List<View> getItemsToDisplay(Folder folder); } } diff --git a/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java b/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java new file mode 100644 index 000000000..de962b021 --- /dev/null +++ b/src/com/android/launcher3/folder/FolderIconPreviewVerifier.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 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.folder; + +import com.android.launcher3.FolderInfo; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.config.FeatureFlags; + +/** + * Verifies whether an item in a Folder is displayed in the FolderIcon preview. + */ +public class FolderIconPreviewVerifier { + + private final int mMaxGridCountX; + private final int mMaxGridCountY; + private final int mMaxItemsPerPage; + private final int[] mGridSize = new int[2]; + + private int mGridCountX; + private boolean mDisplayingUpperLeftQuadrant = false; + + public FolderIconPreviewVerifier(InvariantDeviceProfile profile) { + mMaxGridCountX = profile.numFolderColumns; + mMaxGridCountY = profile.numFolderRows; + mMaxItemsPerPage = mMaxGridCountX * mMaxGridCountY; + } + + public void setFolderInfo(FolderInfo info) { + int numItemsInFolder = info.contents.size(); + mDisplayingUpperLeftQuadrant = FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION + && !FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON + && numItemsInFolder > FolderIcon.NUM_ITEMS_IN_PREVIEW; + + if (mDisplayingUpperLeftQuadrant) { + FolderPagedView.calculateGridSize(info.contents.size(), 0, 0, mMaxGridCountX, + mMaxGridCountY, mMaxItemsPerPage, mGridSize); + mGridCountX = mGridSize[0]; + } + } + + public boolean isItemInPreview(int rank) { + if (mDisplayingUpperLeftQuadrant) { + // Returns true iff the icon is in the 2x2 upper left quadrant of the Folder. + int col = rank % mGridCountX; + int row = rank / mGridCountX; + return col < 2 && row < 2; + } + return rank < FolderIcon.NUM_ITEMS_IN_PREVIEW; + } +} diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java index 532e5a6c7..2a6007a4e 100644 --- a/src/com/android/launcher3/folder/FolderPagedView.java +++ b/src/com/android/launcher3/folder/FolderPagedView.java @@ -65,7 +65,7 @@ public class FolderPagedView extends PagedView { */ private static final float SCROLL_HINT_FRACTION = 0.07f; - private static final int[] sTempPosArray = new int[2]; + private static final int[] sTmpArray = new int[2]; public final boolean mIsRtl; @@ -116,40 +116,58 @@ public class FolderPagedView extends PagedView { } /** - * Sets up the grid size such that {@param count} items can fit in the grid. + * Calculates the grid size such that {@param count} items can fit in the grid. * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while * maintaining the restrictions of {@link #mMaxCountX} & {@link #mMaxCountY}. */ - private void setupContentDimensions(int count) { - mAllocatedContentSize = count; + public static void calculateGridSize(int count, int countX, int countY, int maxCountX, + int maxCountY, int maxItemsPerPage, int[] out) { boolean done; - if (count >= mMaxItemsPerPage) { - mGridCountX = mMaxCountX; - mGridCountY = mMaxCountY; + int gridCountX = countX; + int gridCountY = countY; + + if (count >= maxItemsPerPage) { + gridCountX = maxCountX; + gridCountY = maxCountY; done = true; } else { done = false; } while (!done) { - int oldCountX = mGridCountX; - int oldCountY = mGridCountY; - if (mGridCountX * mGridCountY < count) { + int oldCountX = gridCountX; + int oldCountY = gridCountY; + if (gridCountX * gridCountY < count) { // Current grid is too small, expand it - if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) { - mGridCountX++; - } else if (mGridCountY < mMaxCountY) { - mGridCountY++; + if ((gridCountX <= gridCountY || gridCountY == maxCountY) + && gridCountX < maxCountX) { + gridCountX++; + } else if (gridCountY < maxCountY) { + gridCountY++; } - if (mGridCountY == 0) mGridCountY++; - } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) { - mGridCountY = Math.max(0, mGridCountY - 1); - } else if ((mGridCountX - 1) * mGridCountY >= count) { - mGridCountX = Math.max(0, mGridCountX - 1); + if (gridCountY == 0) gridCountY++; + } else if ((gridCountY - 1) * gridCountX >= count && gridCountY >= gridCountX) { + gridCountY = Math.max(0, gridCountY - 1); + } else if ((gridCountX - 1) * gridCountY >= count) { + gridCountX = Math.max(0, gridCountX - 1); } - done = mGridCountX == oldCountX && mGridCountY == oldCountY; + done = gridCountX == oldCountX && gridCountY == oldCountY; } + out[0] = gridCountX; + out[1] = gridCountY; + } + + /** + * Sets up the grid size such that {@param count} items can fit in the grid. + */ + public void setupContentDimensions(int count) { + mAllocatedContentSize = count; + calculateGridSize(count, mGridCountX, mGridCountY, mMaxCountX, mMaxCountY, mMaxItemsPerPage, + sTmpArray); + mGridCountX = sTmpArray[0]; + mGridCountY = sTmpArray[1]; + // Update grid size for (int i = getPageCount() - 1; i >= 0; i--) { getPageAt(i).setGridSize(mGridCountX, mGridCountY); @@ -310,6 +328,8 @@ public class FolderPagedView extends PagedView { int position = 0; int newX, newY, rank; + FolderIconPreviewVerifier verifier = new FolderIconPreviewVerifier( + Launcher.getLauncher(getContext()).getDeviceProfile().inv); rank = 0; for (int i = 0; i < itemCount; i++) { View v = list.size() > i ? list.get(i) : null; @@ -342,7 +362,7 @@ public class FolderPagedView extends PagedView { currentPage.addViewToCellLayout( v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true); - if (rank < FolderIcon.NUM_ITEMS_IN_PREVIEW && v instanceof BubbleTextView) { + if (verifier.isItemInPreview(rank) && v instanceof BubbleTextView) { ((BubbleTextView) v).verifyHighRes(); } } @@ -396,12 +416,12 @@ public class FolderPagedView extends PagedView { public int findNearestArea(int pixelX, int pixelY) { int pageIndex = getNextPage(); CellLayout page = getPageAt(pageIndex); - page.findNearestArea(pixelX, pixelY, 1, 1, sTempPosArray); + page.findNearestArea(pixelX, pixelY, 1, 1, sTmpArray); if (mFolder.isLayoutRtl()) { - sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1; + sTmpArray[0] = page.getCountX() - sTmpArray[0] - 1; } return Math.min(mAllocatedContentSize - 1, - pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]); + pageIndex * mMaxItemsPerPage + sTmpArray[1] * mGridCountX + sTmpArray[0]); } public boolean isFull() { diff --git a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java index 297203ae2..9c8c2efdb 100644 --- a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java +++ b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java @@ -87,6 +87,11 @@ public class StackFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule { } @Override + public float getIconSize() { + return mBaselineIconSize; + } + + @Override public float scaleForItem(int index, int numItems) { // Scale is determined by the position of the icon in the preview. index = MAX_NUM_ITEMS_IN_PREVIEW - index - 1; @@ -98,10 +103,4 @@ public class StackFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule { public boolean clipToBackground() { return false; } - - @Override - public List<View> getItemsToDisplay(Folder folder) { - List<View> items = folder.getItemsInReadingOrder(); - return items.subList(0, Math.min(items.size(), MAX_NUM_ITEMS_IN_PREVIEW)); - } } diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java index bb136f7a3..492d85373 100644 --- a/src/com/android/launcher3/graphics/DragPreviewProvider.java +++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java @@ -29,7 +29,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppWidgetHostView; import com.android.launcher3.R; import com.android.launcher3.Workspace; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.folder.FolderIcon; /** @@ -138,7 +138,7 @@ public class DragPreviewProvider { } public final void generateDragOutline(Canvas canvas) { - if (ProviderConfig.IS_DOGFOOD_BUILD && generatedDragOutline != null) { + if (FeatureFlags.IS_DOGFOOD_BUILD && generatedDragOutline != null) { throw new RuntimeException("Drag outline generated twice"); } diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java index 8b207bb0c..60bbce406 100644 --- a/src/com/android/launcher3/graphics/DrawableFactory.java +++ b/src/com/android/launcher3/graphics/DrawableFactory.java @@ -85,10 +85,10 @@ public class DrawableFactory { if (Utilities.isAtLeastO()) { try { // Try to load the path from Mask Icon - Drawable maskIcon = context.getDrawable(R.drawable.mask_drawable_wrapper); - maskIcon.setBounds(0, 0, + Drawable icon = context.getDrawable(R.drawable.adaptive_icon_drawable_wrapper); + icon.setBounds(0, 0, PreloadIconDrawable.PATH_SIZE, PreloadIconDrawable.PATH_SIZE); - return (Path) maskIcon.getClass().getMethod("getIconMask").invoke(maskIcon); + return (Path) icon.getClass().getMethod("getIconMask").invoke(icon); } catch (Exception e) { Log.e(TAG, "Error loading mask icon", e); } diff --git a/src/com/android/launcher3/graphics/HolographicOutlineHelper.java b/src/com/android/launcher3/graphics/HolographicOutlineHelper.java index c9873d9ea..b22182883 100644 --- a/src/com/android/launcher3/graphics/HolographicOutlineHelper.java +++ b/src/com/android/launcher3/graphics/HolographicOutlineHelper.java @@ -31,7 +31,7 @@ import android.util.SparseArray; import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import java.nio.ByteBuffer; @@ -86,7 +86,7 @@ public class HolographicOutlineHelper { * bitmap. */ public void applyExpensiveOutlineWithBlur(Bitmap srcDst, Canvas srcDstCanvas) { - if (ProviderConfig.IS_DOGFOOD_BUILD && srcDst.getConfig() != Bitmap.Config.ALPHA_8) { + if (FeatureFlags.IS_DOGFOOD_BUILD && srcDst.getConfig() != Bitmap.Config.ALPHA_8) { throw new RuntimeException("Outline blue is only supported on alpha bitmaps"); } diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java index 23c6a1230..cd7cf702e 100644 --- a/src/com/android/launcher3/graphics/IconPalette.java +++ b/src/com/android/launcher3/graphics/IconPalette.java @@ -19,11 +19,12 @@ package com.android.launcher3.graphics; import android.app.Notification; import android.content.Context; import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.support.v4.graphics.ColorUtils; import android.util.Log; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.util.Themes; /** @@ -41,12 +42,16 @@ public class IconPalette { public final int dominantColor; public final int backgroundColor; + public final ColorMatrixColorFilter backgroundColorMatrixFilter; public final int textColor; public final int secondaryColor; private IconPalette(int color) { dominantColor = color; backgroundColor = getMutedColor(dominantColor); + ColorMatrix backgroundColorMatrix = new ColorMatrix(); + Themes.setColorScaleOnMatrix(backgroundColor, backgroundColorMatrix); + backgroundColorMatrixFilter = new ColorMatrixColorFilter(backgroundColorMatrix); textColor = getTextColorForBackground(backgroundColor); secondaryColor = getLowContrastColor(backgroundColor); } diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java index 1a50dfe14..ef54661d3 100644 --- a/src/com/android/launcher3/graphics/LauncherIcons.java +++ b/src/com/android/launcher3/graphics/LauncherIcons.java @@ -40,7 +40,6 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.model.PackageItemInfo; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; @@ -166,7 +165,7 @@ public class LauncherIcons { * @param scale the scale to apply before drawing {@param icon} on the canvas */ public static Bitmap createIconBitmap(Drawable icon, Context context, float scale) { - icon = wrapToMaskableIconDrawable(context, icon); + icon = wrapToAdaptiveIconDrawable(context, icon); synchronized (sCanvas) { final int iconBitmapSize = LauncherAppState.getIDP(context).iconBitmapSize; @@ -201,7 +200,7 @@ public class LauncherIcons { int textureWidth = iconBitmapSize; int textureHeight = iconBitmapSize; - final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, + Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, Bitmap.Config.ARGB_8888); final Canvas canvas = sCanvas; canvas.setBitmap(bitmap); @@ -218,29 +217,39 @@ public class LauncherIcons { icon.setBounds(sOldBounds); canvas.setBitmap(null); + if (FeatureFlags.ADAPTIVE_ICON_SHADOW && Utilities.isAtLeastO()) { + try { + Class clazz = Class.forName("android.graphics.drawable.AdaptiveIconDrawable"); + if (clazz.isAssignableFrom(icon.getClass())) { + bitmap = ShadowGenerator.getInstance(context).recreateIcon(bitmap); + } + } catch (Exception e) { + // do nothing + } + } return bitmap; } } /** - * If the platform is running O but the app is not providing MaskableIconDrawable, then + * If the platform is running O but the app is not providing AdaptiveIconDrawable, then * shrink the legacy icon and set it as foreground. Use color drawable as background to - * create MaskableIconDrawable. + * create AdaptiveIconDrawable. */ - static Drawable wrapToMaskableIconDrawable(Context context, Drawable drawable) { + static Drawable wrapToAdaptiveIconDrawable(Context context, Drawable drawable) { if (!(FeatureFlags.LEGACY_ICON_TREATMENT && Utilities.isAtLeastO())) { return drawable; } try { - Class clazz = Class.forName("android.graphics.drawable.MaskableIconDrawable"); + Class clazz = Class.forName("android.graphics.drawable.AdaptiveIconDrawable"); if (!clazz.isAssignableFrom(drawable.getClass())) { - Drawable maskWrapper = - context.getDrawable(R.drawable.mask_drawable_wrapper).mutate(); - ((FixedScaleDrawable) clazz.getMethod("getForeground").invoke(maskWrapper)) + Drawable iconWrapper = + context.getDrawable(R.drawable.adaptive_icon_drawable_wrapper).mutate(); + ((FixedScaleDrawable) clazz.getMethod("getForeground").invoke(iconWrapper)) .setDrawable(drawable); - return maskWrapper; + return iconWrapper; } } catch (Exception e) { return drawable; diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java index 3514a37c4..22ce0981d 100644 --- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java +++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java @@ -217,6 +217,9 @@ public class PreloadIconDrawable extends FastBitmapDrawable { if (Float.compare(finalProgress, mInternalStateProgress) == 0) { return; } + if (finalProgress < mInternalStateProgress) { + shouldAnimate = false; + } if (!shouldAnimate || mRanFinishAnimation) { setInternalProgress(finalProgress); } else { diff --git a/src/com/android/launcher3/graphics/ShadowGenerator.java b/src/com/android/launcher3/graphics/ShadowGenerator.java index 31276ecc0..6c603c971 100644 --- a/src/com/android/launcher3/graphics/ShadowGenerator.java +++ b/src/com/android/launcher3/graphics/ShadowGenerator.java @@ -83,6 +83,38 @@ public class ShadowGenerator { return result; } + public static Bitmap createCircleWithShadow(int circleColor, int diameter) { + + float shadowRadius = diameter * 1f / 32; + float shadowYOffset = diameter * 1f / 16; + + int radius = diameter / 2; + + Canvas canvas = new Canvas(); + Paint blurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + blurPaint.setMaskFilter(new BlurMaskFilter(shadowRadius, Blur.NORMAL)); + + int center = Math.round(radius + shadowRadius + shadowYOffset); + int size = center * 2; + Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888); + canvas.setBitmap(result); + + // Draw ambient shadow, center aligned within size + blurPaint.setAlpha(AMBIENT_SHADOW_ALPHA); + canvas.drawCircle(center, center, radius, blurPaint); + + // Draw key shadow, bottom aligned within size + blurPaint.setAlpha(KEY_SHADOW_ALPHA); + canvas.drawCircle(center, center + shadowYOffset, radius, blurPaint); + + // Draw the circle + Paint drawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + drawPaint.setColor(circleColor); + canvas.drawCircle(center, center, radius, drawPaint); + + return result; + } + public static ShadowGenerator getInstance(Context context) { Preconditions.assertNonUiThread(); synchronized (LOCK) { diff --git a/src/com/android/launcher3/logging/DumpTargetWrapper.java b/src/com/android/launcher3/logging/DumpTargetWrapper.java new file mode 100644 index 000000000..2646a2242 --- /dev/null +++ b/src/com/android/launcher3/logging/DumpTargetWrapper.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2017 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.logging; + +import android.os.Process; +import android.text.TextUtils; + +import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.model.nano.LauncherDumpProto; +import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType; +import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget; +import com.android.launcher3.model.nano.LauncherDumpProto.ItemType; +import com.android.launcher3.model.nano.LauncherDumpProto.UserType; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class can be used when proto definition doesn't support nesting. + */ +public class DumpTargetWrapper { + DumpTarget node; + ArrayList<DumpTargetWrapper> children; + + public DumpTargetWrapper() { + children = new ArrayList<>(); + } + + public DumpTargetWrapper(DumpTarget t) { + this(); + node = t; + } + + public DumpTargetWrapper(int containerType, int id) { + this(); + node = newContainerTarget(containerType, id); + } + + public DumpTargetWrapper(ItemInfo info) { + this(); + node = newItemTarget(info); + } + + public DumpTarget getDumpTarget() { + return node; + } + + public void add(DumpTargetWrapper child) { + children.add(child); + } + + public List<DumpTarget> getFlattenedList() { + ArrayList<DumpTarget> list = new ArrayList<>(); + list.add(node); + if (!children.isEmpty()) { + for(DumpTargetWrapper t: children) { + list.addAll(t.getFlattenedList()); + } + list.add(node); // add a delimiter empty object + } + return list; + } + public DumpTarget newItemTarget(ItemInfo info) { + DumpTarget dt = new DumpTarget(); + dt.type = DumpTarget.Type.ITEM; + + switch (info.itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: + dt.itemType = ItemType.APP_ICON; + break; + case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: + dt.itemType = ItemType.UNKNOWN_ITEMTYPE; + break; + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + dt.itemType = ItemType.WIDGET; + break; + case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: + dt.itemType = ItemType.SHORTCUT; + break; + } + return dt; + } + + public DumpTarget newContainerTarget(int type, int id) { + DumpTarget dt = new DumpTarget(); + dt.type = DumpTarget.Type.CONTAINER; + dt.containerType = type; + dt.pageId = id; + return dt; + } + + public static String getDumpTargetStr(DumpTarget t) { + if (t == null){ + return ""; + } + switch (t.type) { + case LauncherDumpProto.DumpTarget.Type.ITEM: + return getItemStr(t); + case LauncherDumpProto.DumpTarget.Type.CONTAINER: + String str = LoggerUtils.getFieldName(t.containerType, ContainerType.class); + if (t.containerType == ContainerType.WORKSPACE) { + str += " id=" + t.pageId; + } else if (t.containerType == ContainerType.FOLDER) { + str += " grid(" + t.gridX + "," + t.gridY+ ")"; + } + return str; + default: + return "UNKNOWN TARGET TYPE"; + } + } + + private static String getItemStr(DumpTarget t) { + String typeStr = LoggerUtils.getFieldName(t.itemType, ItemType.class); + if (!TextUtils.isEmpty(t.packageName)) { + typeStr += ", package=" + t.packageName; + } + if (!TextUtils.isEmpty(t.component)) { + typeStr += ", component=" + t.component; + } + return typeStr + ", grid(" + t.gridX + "," + t.gridY + "), span(" + t.spanX + "," + t.spanY + + "), pageIdx=" + t.pageId + " user=" + t.userType; + } + + public DumpTarget writeToDumpTarget(ItemInfo info) { + node.component = info.getTargetComponent() == null? "": + info.getTargetComponent().flattenToString(); + node.packageName = info.getIntent() == null? "": info.getIntent().getPackage(); + node.gridX = info.cellX; + node.gridY = info.cellY; + node.spanX = info.spanX; + node.spanY = info.spanY; + node.userType = (info.user.equals(Process.myUserHandle()))? UserType.DEFAULT : UserType.WORK; + return node; + } +} diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java index ffb41b76b..4c83e9ac2 100644 --- a/src/com/android/launcher3/logging/FileLog.java +++ b/src/com/android/launcher3/logging/FileLog.java @@ -6,9 +6,8 @@ import android.os.Message; import android.util.Log; import android.util.Pair; -import com.android.launcher3.LauncherModel; import com.android.launcher3.Utilities; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import java.io.BufferedReader; import java.io.File; @@ -40,7 +39,7 @@ public final class FileLog { private static File sLogsDirectory = null; public static void setDir(File logsDir) { - if (ProviderConfig.IS_DOGFOOD_BUILD || Utilities.IS_DEBUG_DEVICE) { + if (FeatureFlags.IS_DOGFOOD_BUILD || Utilities.IS_DEBUG_DEVICE) { synchronized (DATE_FORMAT) { // If the target directory changes, stop any active thread. if (sHandler != null && !logsDir.equals(sLogsDirectory)) { @@ -77,7 +76,7 @@ public final class FileLog { } public static void print(String tag, String msg, Exception e) { - if (!ProviderConfig.IS_DOGFOOD_BUILD) { + if (!FeatureFlags.IS_DOGFOOD_BUILD) { return; } String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg); @@ -103,7 +102,7 @@ public final class FileLog { * @param out if not null, all the persisted logs are copied to the writer. */ public static void flushAll(PrintWriter out) throws InterruptedException { - if (!ProviderConfig.IS_DOGFOOD_BUILD) { + if (!FeatureFlags.IS_DOGFOOD_BUILD) { return; } CountDownLatch latch = new CountDownLatch(1); @@ -136,7 +135,7 @@ public final class FileLog { @Override public boolean handleMessage(Message msg) { - if (sLogsDirectory == null || !ProviderConfig.IS_DOGFOOD_BUILD) { + if (sLogsDirectory == null || !FeatureFlags.IS_DOGFOOD_BUILD) { return true; } switch (msg.what) { diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java index c13e8b336..499fdc7d3 100644 --- a/src/com/android/launcher3/logging/LoggerUtils.java +++ b/src/com/android/launcher3/logging/LoggerUtils.java @@ -1,5 +1,21 @@ +/* + * Copyright (C) 2016 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.logging; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.SparseArray; import android.view.View; @@ -27,7 +43,7 @@ public class LoggerUtils { private static final ArrayMap<Class, SparseArray<String>> sNameCache = new ArrayMap<>(); private static final String UNKNOWN = "UNKNOWN"; - private static String getFieldName(int value, Class c) { + public static String getFieldName(int value, Class c) { SparseArray<String> cache; synchronized (sNameCache) { cache = sNameCache.get(c); @@ -68,8 +84,13 @@ public class LoggerUtils { case Target.Type.CONTROL: return getFieldName(t.controlType, ControlType.class); case Target.Type.CONTAINER: - return getFieldName(t.containerType, ContainerType.class) - + " id=" + t.pageIndex; + String str = getFieldName(t.containerType, ContainerType.class); + if (t.containerType == ContainerType.WORKSPACE) { + str += " id=" + t.pageIndex; + } else if (t.containerType == ContainerType.FOLDER) { + str += " grid(" + t.gridX + "," + t.gridY+ ")"; + } + return str; default: return "UNKNOWN TARGET TYPE"; } @@ -86,10 +107,8 @@ public class LoggerUtils { if (t.intentHash != 0) { typeStr += ", intentHash=" + t.intentHash; } - if (t.spanX != 0) { - typeStr += ", spanX=" + t.spanX; - } - return typeStr + ", grid=(" + t.gridX + "," + t.gridY + "), id=" + t.pageIndex; + return typeStr + ", grid(" + t.gridX + "," + t.gridY + "), span(" + t.spanX + "," + t.spanY + + "), pageIdx=" + t.pageIndex; } public static Target newItemTarget(View v) { diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java index 5f45c6176..04ca24741 100644 --- a/src/com/android/launcher3/logging/UserEventDispatcher.java +++ b/src/com/android/launcher3/logging/UserEventDispatcher.java @@ -18,6 +18,7 @@ package com.android.launcher3.logging; import android.app.PendingIntent; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.os.SystemClock; import android.util.Log; @@ -26,9 +27,9 @@ import android.view.ViewParent; import com.android.launcher3.DropTarget; import com.android.launcher3.ItemInfo; +import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.config.ProviderConfig; -import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.userevent.nano.LauncherLogProto.LauncherEvent; @@ -59,7 +60,20 @@ public class UserEventDispatcher { private static final String TAG = "UserEvent"; private static final boolean IS_VERBOSE = - ProviderConfig.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.USEREVENT); + FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.USEREVENT); + + private static UserEventDispatcher sInstance; + private static final Object LOCK = new Object(); + + public static UserEventDispatcher get(Context context) { + synchronized (LOCK) { + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(UserEventDispatcher.class, + context.getApplicationContext(), R.string.user_event_dispatcher_class); + } + return sInstance; + } + } /** * Implemented by containers to provide a container source for a given child. diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 6b64087a2..930c854c3 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -24,19 +24,25 @@ import android.util.MutableInt; import com.android.launcher3.FolderInfo; import com.android.launcher3.InstallShortcutReceiver; import com.android.launcher3.ItemInfo; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherSettings; import com.android.launcher3.ShortcutInfo; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.logging.DumpTargetWrapper; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.model.nano.LauncherDumpProto; +import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType; +import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.MultiHashMap; +import com.google.protobuf.nano.MessageNano; import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -102,21 +108,31 @@ public class BgDataModel { deepShortcutMap.clear(); } - // TODO: current dump is very cryptic and hard to understand. Make it more legible. - public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer, + String[] args) { + if (args.length > 0 && TextUtils.equals(args[0], "--proto")) { + dumpProto(prefix, fd, writer, args); + return; + } writer.println(prefix + "Data Model:"); + writer.print(prefix + " ---- workspace screens: "); for (int i = 0; i < workspaceScreens.size(); i++) { - writer.println(prefix + "\tIndex of workspaceScreens:" + workspaceScreens.get(i).toString()); + writer.print(" " + workspaceScreens.get(i).toString()); } + writer.println(); + writer.println(prefix + " ---- workspace items "); for (int i = 0; i < workspaceItems.size(); i++) { writer.println(prefix + '\t' + workspaceItems.get(i).toString()); } + writer.println(prefix + " ---- appwidget items "); for (int i = 0; i < appWidgets.size(); i++) { writer.println(prefix + '\t' + appWidgets.get(i).toString()); } + writer.println(prefix + " ---- folder items "); for (int i = 0; i< folders.size(); i++) { writer.println(prefix + '\t' + folders.valueAt(i).toString()); } + writer.println(prefix + " ---- items id map "); for (int i = 0; i< itemsIdMap.size(); i++) { writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString()); } @@ -133,6 +149,88 @@ public class BgDataModel { } } + private synchronized void dumpProto(String prefix, FileDescriptor fd, PrintWriter writer, + String[] args) { + + // Add top parent nodes. (L1) + DumpTargetWrapper hotseat = new DumpTargetWrapper(ContainerType.HOTSEAT, 0); + LongArrayMap<DumpTargetWrapper> workspaces = new LongArrayMap<>(); + for (int i = 0; i < workspaceScreens.size(); i++) { + workspaces.put(new Long(workspaceScreens.get(i)), + new DumpTargetWrapper(ContainerType.WORKSPACE, i)); + } + DumpTargetWrapper dtw; + // Add non leaf / non top nodes (L2) + for (int i = 0; i < folders.size(); i++) { + FolderInfo fInfo = folders.valueAt(i); + dtw = new DumpTargetWrapper(ContainerType.FOLDER, folders.size()); + dtw.writeToDumpTarget(fInfo); + for(ShortcutInfo sInfo: fInfo.contents) { + DumpTargetWrapper child = new DumpTargetWrapper(sInfo); + child.writeToDumpTarget(sInfo); + dtw.add(child); + } + if (fInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + hotseat.add(dtw); + } else if (fInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + workspaces.get(new Long(fInfo.screenId)).add(dtw); + } + } + // Add leaf nodes (L3): *Info + for (int i = 0; i < workspaceItems.size(); i++) { + ItemInfo info = workspaceItems.get(i); + if (info instanceof FolderInfo) { + continue; + } + dtw = new DumpTargetWrapper(info); + dtw.writeToDumpTarget(info); + if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + hotseat.add(dtw); + } else if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + workspaces.get(new Long(info.screenId)).add(dtw); + } + } + for (int i = 0; i < appWidgets.size(); i++) { + ItemInfo info = appWidgets.get(i); + dtw = new DumpTargetWrapper(info); + dtw.writeToDumpTarget(info); + if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { + hotseat.add(dtw); + } else if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { + workspaces.get(new Long(info.screenId)).add(dtw); + } + } + + + // Traverse target wrapper + ArrayList<DumpTarget> targetList = new ArrayList<>(); + targetList.addAll(hotseat.getFlattenedList()); + for (int i = 0; i < workspaces.size(); i++) { + targetList.addAll(workspaces.valueAt(i).getFlattenedList()); + } + + if (args.length > 1 && TextUtils.equals(args[1], "--debug")) { + for (int i = 0; i < targetList.size(); i++) { + writer.println(prefix + DumpTargetWrapper.getDumpTargetStr(targetList.get(i))); + } + return; + } else { + LauncherDumpProto.LauncherImpression proto = new LauncherDumpProto.LauncherImpression(); + proto.targets = new DumpTarget[targetList.size()]; + for (int i = 0; i < targetList.size(); i++) { + proto.targets[i] = targetList.get(i); + } + FileOutputStream fos = new FileOutputStream(fd); + try { + + fos.write(MessageNano.toByteArray(proto)); + Log.d(TAG, MessageNano.toByteArray(proto).length + "Bytes"); + } catch (IOException e) { + Log.e(TAG, "Exception writing dumpsys --proto", e); + } + } + } + public synchronized void removeItem(Context context, ItemInfo... items) { removeItem(context, Arrays.asList(items)); } @@ -142,7 +240,7 @@ public class BgDataModel { switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: folders.remove(item.id); - if (ProviderConfig.IS_DOGFOOD_BUILD) { + if (FeatureFlags.IS_DOGFOOD_BUILD) { for (ItemInfo info : itemsIdMap) { if (info.container == item.id) { // We are deleting a folder which still contains items that diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 4931dca14..032ed780d 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -34,7 +34,7 @@ import com.android.launcher3.LauncherSettings.Settings; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.LooperExecuter; +import com.android.launcher3.util.LooperExecutor; import java.util.ArrayList; import java.util.Arrays; @@ -55,7 +55,7 @@ public class ModelWriter { public ModelWriter(Context context, BgDataModel dataModel, boolean hasVerticalHotseat) { mContext = context; mBgDataModel = dataModel; - mWorkerExecutor = new LooperExecuter(LauncherModel.getWorkerLooper()); + mWorkerExecutor = new LooperExecutor(LauncherModel.getWorkerLooper()); mHasVerticalHotseat = hasVerticalHotseat; } diff --git a/src/com/android/launcher3/model/SdCardAvailableReceiver.java b/src/com/android/launcher3/model/SdCardAvailableReceiver.java index 7c9836282..278669bdb 100644 --- a/src/com/android/launcher3/model/SdCardAvailableReceiver.java +++ b/src/com/android/launcher3/model/SdCardAvailableReceiver.java @@ -63,7 +63,7 @@ public class SdCardAvailableReceiver extends BroadcastReceiver { for (String pkg : new HashSet<>(entry.getValue())) { if (!launcherApps.isPackageEnabledForProfile(pkg, user)) { - if (pmHelper.isAppOnSdcard(pkg)) { + if (pmHelper.isAppOnSdcard(pkg, user)) { packagesUnavailable.add(pkg); } else { packagesRemoved.add(pkg); diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java index 95c54f767..d2adbde23 100644 --- a/src/com/android/launcher3/model/WidgetsModel.java +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -3,9 +3,7 @@ package com.android.launcher3.model; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.Process; import android.os.UserHandle; import android.util.Log; @@ -19,8 +17,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.ShortcutConfigActivityInfo; -import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.Preconditions; @@ -79,7 +76,7 @@ public class WidgetsModel { } setWidgetsAndShortcuts(widgetsAndShortcuts, context); } catch (Exception e) { - if (!ProviderConfig.IS_DOGFOOD_BUILD && Utilities.isBinderSizeError(e)) { + if (!FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isBinderSizeError(e)) { // the returned value may be incomplete and will not be refreshed until the next // time Launcher starts. // TODO: after figuring out a repro step, introduce a dirty bit to check when diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java index 57ec5d17c..2e803417d 100644 --- a/src/com/android/launcher3/notification/NotificationFooterLayout.java +++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java @@ -21,19 +21,22 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; -import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.TextView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.anim.PropertyListBuilder; -import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.anim.PropertyResetListener; import com.android.launcher3.popup.PopupContainerWithArrow; import java.util.ArrayList; @@ -41,10 +44,10 @@ import java.util.Iterator; import java.util.List; /** - * A {@link LinearLayout} that contains only icons of notifications. - * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "+x" overflow. + * A {@link FrameLayout} that contains only icons of notifications. + * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "..." overflow. */ -public class NotificationFooterLayout extends LinearLayout { +public class NotificationFooterLayout extends FrameLayout { public interface IconAnimationEndListener { void onIconAnimationEnd(NotificationInfo animatedNotification); @@ -56,11 +59,12 @@ public class NotificationFooterLayout extends LinearLayout { private final List<NotificationInfo> mNotifications = new ArrayList<>(); private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>(); + private final boolean mRtl; - LinearLayout.LayoutParams mIconLayoutParams; + FrameLayout.LayoutParams mIconLayoutParams; + private View mOverflowEllipsis; private LinearLayout mIconRow; private int mBackgroundColor; - private int mTextColor; public NotificationFooterLayout(Context context) { this(context, null, 0); @@ -73,26 +77,29 @@ public class NotificationFooterLayout extends LinearLayout { public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - int size = getResources().getDimensionPixelSize( - R.dimen.notification_footer_icon_size); - int padding = getResources().getDimensionPixelSize( - R.dimen.deep_shortcut_drawable_padding); - mIconLayoutParams = new LayoutParams(size, size); - mIconLayoutParams.setMarginStart(padding); + Resources res = getResources(); + mRtl = Utilities.isRtl(res); + + int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size); + mIconLayoutParams = new LayoutParams(iconSize, iconSize); mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL; + // Compute margin start for each icon such that the icons between the first one + // and the ellipsis are evenly spaced out. + int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding); + int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset) + + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size); + int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width); + int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace + - iconSize * MAX_FOOTER_NOTIFICATIONS; + mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS); } @Override protected void onFinishInflate() { super.onFinishInflate(); + mOverflowEllipsis = findViewById(R.id.overflow); mIconRow = (LinearLayout) findViewById(R.id.icon_row); - } - - public void applyColors(IconPalette iconPalette) { - mBackgroundColor = iconPalette.backgroundColor; - setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); - findViewById(R.id.divider).setBackgroundColor(iconPalette.secondaryColor); - mTextColor = iconPalette.textColor; + mBackgroundColor = ((ColorDrawable) getBackground()).getColor(); } /** @@ -116,41 +123,32 @@ public class NotificationFooterLayout extends LinearLayout { for (int i = 0; i < mNotifications.size(); i++) { NotificationInfo info = mNotifications.get(i); - addNotificationIconForInfo(info, false /* fromOverflow */); + addNotificationIconForInfo(info); } + updateOverflowEllipsisVisibility(); + } - if (!mOverflowNotifications.isEmpty()) { - TextView overflowText = new TextView(getContext()); - overflowText.setTextColor(mTextColor); - updateOverflowText(overflowText); - mIconRow.addView(overflowText, mIconLayoutParams); - } + private void updateOverflowEllipsisVisibility() { + mOverflowEllipsis.setVisibility(mOverflowNotifications.isEmpty() ? GONE : VISIBLE); } - private void addNotificationIconForInfo(NotificationInfo info, boolean fromOverflow) { + /** + * Creates an icon for the given NotificationInfo, and adds it to the icon row. + * @return the icon view that was added + */ + private View addNotificationIconForInfo(NotificationInfo info) { View icon = new View(getContext()); icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor)); icon.setOnClickListener(info); - int addIndex = mIconRow.getChildCount(); - if (fromOverflow) { - // Add the notification before the overflow view. - addIndex--; - icon.setAlpha(0); - icon.animate().alpha(1); - } icon.setTag(info); - mIconRow.addView(icon, addIndex, mIconLayoutParams); - } - - private void updateOverflowText(TextView overflowTextView) { - overflowTextView.setText(getResources().getString(R.string.deep_notifications_overflow, - mOverflowNotifications.size())); + mIconRow.addView(icon, 0, mIconLayoutParams); + return icon; } public void animateFirstNotificationTo(Rect toBounds, final IconAnimationEndListener callback) { AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); - final View firstNotification = mIconRow.getChildAt(0); + final View firstNotification = mIconRow.getChildAt(mIconRow.getChildCount() - 1); Rect fromBounds = sTempRect; firstNotification.getGlobalVisibleRect(fromBounds); @@ -162,29 +160,54 @@ public class NotificationFooterLayout extends LinearLayout { @Override public void onAnimationEnd(Animator animation) { callback.onIconAnimationEnd((NotificationInfo) firstNotification.getTag()); + removeViewFromIconRow(firstNotification); } }); animation.play(moveAndScaleIcon); // Shift all notifications (not the overflow) over to fill the gap. int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart(); - int numIcons = mIconRow.getChildCount() - - (mOverflowNotifications.isEmpty() ? 0 : 1); - for (int i = 1; i < numIcons; i++) { + if (mRtl) { + gapWidth = -gapWidth; + } + if (!mOverflowNotifications.isEmpty()) { + NotificationInfo notification = mOverflowNotifications.remove(0); + mNotifications.add(notification); + View iconFromOverflow = addNotificationIconForInfo(notification); + animation.play(ObjectAnimator.ofFloat(iconFromOverflow, ALPHA, 0, 1)); + } + int numIcons = mIconRow.getChildCount() - 1; // All children besides the one leaving. + // We have to reset the translation X to 0 when the new main notification + // is removed from the footer. + PropertyResetListener<View, Float> propertyResetListener + = new PropertyResetListener<>(TRANSLATION_X, 0f); + for (int i = 0; i < numIcons; i++) { final View child = mIconRow.getChildAt(i); - Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, -gapWidth); - shiftChild.addListener(new AnimatorListenerAdapter() { + Animator shiftChild = ObjectAnimator.ofFloat(child, TRANSLATION_X, gapWidth); + shiftChild.addListener(propertyResetListener); + animation.play(shiftChild); + } + animation.start(); + } + + private void removeViewFromIconRow(View child) { + mIconRow.removeView(child); + mNotifications.remove((NotificationInfo) child.getTag()); + updateOverflowEllipsisVisibility(); + if (mIconRow.getChildCount() == 0) { + // There are no more icons in the footer, so hide it. + PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen( + Launcher.getLauncher(getContext())); + Animator collapseFooter = popup.reduceNotificationViewHeight(getHeight(), + getResources().getInteger(R.integer.config_removeNotificationViewDuration)); + collapseFooter.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - // We have to set the translation X to 0 when the new main notification - // is removed from the footer. - // TODO: remove it here instead of expecting trimNotifications to do so. - child.setTranslationX(0); + ((ViewGroup) getParent()).removeView(NotificationFooterLayout.this); } }); - animation.play(shiftChild); + collapseFooter.start(); } - animation.start(); } public void trimNotifications(List<String> notifications) { @@ -197,43 +220,12 @@ public class NotificationFooterLayout extends LinearLayout { overflowIterator.remove(); } } - TextView overflowView = null; for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) { View child = mIconRow.getChildAt(i); - if (child instanceof TextView) { - overflowView = (TextView) child; - } else { - NotificationInfo childInfo = (NotificationInfo) child.getTag(); - if (!notifications.contains(childInfo.notificationKey)) { - mIconRow.removeView(child); - mNotifications.remove(childInfo); - if (!mOverflowNotifications.isEmpty()) { - NotificationInfo notification = mOverflowNotifications.remove(0); - mNotifications.add(notification); - addNotificationIconForInfo(notification, true /* fromOverflow */); - } - } - } - } - if (overflowView != null) { - if (mOverflowNotifications.isEmpty()) { - mIconRow.removeView(overflowView); - } else { - updateOverflowText(overflowView); + NotificationInfo childInfo = (NotificationInfo) child.getTag(); + if (!notifications.contains(childInfo.notificationKey)) { + removeViewFromIconRow(child); } } - if (mIconRow.getChildCount() == 0) { - // There are no more icons in the secondary view, so hide it. - PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen( - Launcher.getLauncher(getContext())); - int newHeight = getResources().getDimensionPixelSize( - R.dimen.notification_footer_collapsed_height); - AnimatorSet collapseSecondary = LauncherAnimUtils.createAnimatorSet(); - collapseSecondary.play(popup.animateTranslationYBy(getHeight() - newHeight, - getResources().getInteger(R.integer.config_removeNotificationViewDuration))); - collapseSecondary.play(LauncherAnimUtils.animateViewHeight( - this, getHeight(), newHeight)); - collapseSecondary.start(); - } } } diff --git a/src/com/android/launcher3/notification/NotificationHeaderView.java b/src/com/android/launcher3/notification/NotificationHeaderView.java new file mode 100644 index 000000000..e70b48949 --- /dev/null +++ b/src/com/android/launcher3/notification/NotificationHeaderView.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017 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.notification; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.R; + +/** + * A {@link LinearLayout} that contains two text views: one for the notification count + * and one just to say "Notification" or "Notifications" + */ +public class NotificationHeaderView extends LinearLayout { + + private TextView mNotificationCount; + private TextView mNotificationText; + + public NotificationHeaderView(Context context) { + this(context, null, 0); + } + + public NotificationHeaderView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NotificationHeaderView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mNotificationCount = (TextView) findViewById(R.id.notification_count); + mNotificationText = (TextView) findViewById(R.id.notification_text); + } + + public void update(int notificationCount) { + mNotificationCount.setText(String.valueOf(notificationCount)); + mNotificationText.setText(getResources().getQuantityString( + R.plurals.notifications_header, notificationCount)); + } +} diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java index c9b3940b8..a34074271 100644 --- a/src/com/android/launcher3/notification/NotificationItemView.java +++ b/src/com/android/launcher3/notification/NotificationItemView.java @@ -18,30 +18,23 @@ package com.android.launcher3.notification; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.content.Context; -import android.content.res.ColorStateList; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.view.animation.LinearInterpolator; +import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.TextView; import com.android.launcher3.ItemInfo; -import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; -import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.anim.PillHeightRevealOutlineProvider; import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider; import com.android.launcher3.popup.PopupItemView; import com.android.launcher3.userevent.nano.LauncherLogProto; -import java.util.ArrayList; import java.util.List; -import static com.android.launcher3.LauncherAnimUtils.animateViewHeight; - /** * A {@link FrameLayout} that contains a header, main view and a footer. * The main view contains the icon and text (title + subtext) of the first notification. @@ -52,8 +45,7 @@ public class NotificationItemView extends PopupItemView implements LogContainerP private static final Rect sTempRect = new Rect(); - private TextView mHeader; - private View mDivider; + private NotificationHeaderView mHeader; private NotificationMainView mMainView; private NotificationFooterLayout mFooter; private SwipeHelper mSwipeHelper; @@ -74,11 +66,35 @@ public class NotificationItemView extends PopupItemView implements LogContainerP @Override protected void onFinishInflate() { super.onFinishInflate(); - mHeader = (TextView) findViewById(R.id.header); - mDivider = findViewById(R.id.divider); + mHeader = (NotificationHeaderView) findViewById(R.id.header); mMainView = (NotificationMainView) findViewById(R.id.main_view); mFooter = (NotificationFooterLayout) findViewById(R.id.footer); mSwipeHelper = new SwipeHelper(SwipeHelper.X, mMainView, getContext()); + mSwipeHelper.setDisableHardwareLayers(true); + } + + public Animator animateHeightRemoval(int heightToRemove) { + final int newHeight = getHeight() - heightToRemove; + Animator heightAnimator = new PillHeightRevealOutlineProvider(mPillRect, + getBackgroundRadius(), newHeight).createRevealAnimator(this, true /* isReversed */); + heightAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (newHeight > 0) { + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + initializeBackgroundClipping(true /* force */); + invalidate(); + } else { + ((ViewGroup) getParent()).removeView(NotificationItemView.this); + } + } + }); + return heightAnimator; + } + + public void updateHeader(int notificationCount) { + mHeader.update(notificationCount); } @Override @@ -100,13 +116,6 @@ public class NotificationItemView extends PopupItemView implements LogContainerP return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev); } - @Override - protected ColorStateList getAttachedArrowColor() { - // This NotificationView itself has a different color that is only - // revealed when dismissing notifications. - return mFooter.getBackgroundTintList(); - } - public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) { if (notificationInfos.isEmpty()) { return; @@ -121,15 +130,6 @@ public class NotificationItemView extends PopupItemView implements LogContainerP mFooter.commitNotificationInfos(); } - public void applyColors(IconPalette iconPalette) { - setBackgroundTintList(ColorStateList.valueOf(iconPalette.secondaryColor)); - mHeader.setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor)); - mHeader.setTextColor(ColorStateList.valueOf(iconPalette.textColor)); - mDivider.setBackgroundColor(iconPalette.secondaryColor); - mMainView.applyColors(iconPalette); - mFooter.applyColors(iconPalette); - } - public void trimNotifications(final List<String> notificationKeys) { boolean dismissedMainNotification = !notificationKeys.contains( mMainView.getNotificationInfo().notificationKey); @@ -145,12 +145,6 @@ public class NotificationItemView extends PopupItemView implements LogContainerP public void onIconAnimationEnd(NotificationInfo newMainNotification) { if (newMainNotification != null) { mMainView.applyNotificationInfo(newMainNotification, mIconView, true); - // Remove the animated notification from the footer by calling trim - // TODO: Remove the notification in NotificationFooterLayout directly - // instead of relying on this hack. - List<String> footerNotificationKeys = new ArrayList<>(notificationKeys); - footerNotificationKeys.remove(newMainNotification.notificationKey); - mFooter.trimNotifications(footerNotificationKeys); mMainView.setVisibility(VISIBLE); } mAnimatingNextIcon = false; @@ -161,27 +155,6 @@ public class NotificationItemView extends PopupItemView implements LogContainerP } } - public Animator createRemovalAnimation(int fullDuration) { - AnimatorSet animation = new AnimatorSet(); - int mainHeight = mMainView.getMeasuredHeight(); - Animator removeMainView = animateViewHeight(mMainView, mainHeight, 0); - removeMainView.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Remove the remaining views but take on their color instead of the darker one. - setBackgroundTintList(mHeader.getBackgroundTintList()); - removeAllViews(); - } - }); - Animator removeRest = LauncherAnimUtils.animateViewHeight(this, getHeight() - mainHeight, 0); - removeMainView.setDuration(fullDuration / 2); - removeRest.setDuration(fullDuration / 2); - removeMainView.setInterpolator(new LinearInterpolator()); - removeRest.setInterpolator(new LinearInterpolator()); - animation.playSequentially(removeMainView, removeRest); - return animation; - } - @Override public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target, LauncherLogProto.Target targetParent) { diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index 5c16176f5..d9f7d7634 100644 --- a/src/com/android/launcher3/notification/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -26,6 +26,7 @@ import android.support.annotation.Nullable; import android.support.v4.util.Pair; import com.android.launcher3.LauncherModel; +import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.PackageUserKey; @@ -40,7 +41,7 @@ import java.util.Set; * A {@link NotificationListenerService} that sends updates to its * {@link NotificationsChangedListener} when notifications are posted or canceled, * as well and when this service first connects. An instance of NotificationListener, - * and its methods for getting notifications, can be obtained via {@link #getInstance()}. + * and its methods for getting notifications, can be obtained via {@link #getInstanceIfConnected()}. */ public class NotificationListener extends NotificationListenerService { @@ -50,10 +51,13 @@ public class NotificationListener extends NotificationListenerService { private static NotificationListener sNotificationListenerInstance = null; private static NotificationsChangedListener sNotificationsChangedListener; + private static boolean sIsConnected; private final Handler mWorkerHandler; private final Handler mUiHandler; + private Ranking mTempRanking = new Ranking(); + private Handler.Callback mWorkerCallback = new Handler.Callback() { @Override public boolean handleMessage(Message message) { @@ -65,8 +69,9 @@ public class NotificationListener extends NotificationListenerService { mUiHandler.obtainMessage(message.what, message.obj).sendToTarget(); break; case MSG_NOTIFICATION_FULL_REFRESH: - final List<StatusBarNotification> activeNotifications - = filterNotifications(getActiveNotifications()); + final List<StatusBarNotification> activeNotifications = sIsConnected + ? filterNotifications(getActiveNotifications()) + : new ArrayList<StatusBarNotification>(); mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget(); break; } @@ -107,10 +112,11 @@ public class NotificationListener extends NotificationListenerService { super(); mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback); mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback); + sNotificationListenerInstance = this; } - public static @Nullable NotificationListener getInstance() { - return sNotificationListenerInstance; + public static @Nullable NotificationListener getInstanceIfConnected() { + return sIsConnected ? sNotificationListenerInstance : null; } public static void setNotificationsChangedListener(NotificationsChangedListener listener) { @@ -119,9 +125,8 @@ public class NotificationListener extends NotificationListenerService { } sNotificationsChangedListener = listener; - NotificationListener notificationListener = getInstance(); - if (notificationListener != null) { - notificationListener.onNotificationFullRefresh(); + if (sNotificationListenerInstance != null) { + sNotificationListenerInstance.onNotificationFullRefresh(); } } @@ -132,7 +137,7 @@ public class NotificationListener extends NotificationListenerService { @Override public void onListenerConnected() { super.onListenerConnected(); - sNotificationListenerInstance = this; + sIsConnected = true; onNotificationFullRefresh(); } @@ -143,7 +148,7 @@ public class NotificationListener extends NotificationListenerService { @Override public void onListenerDisconnected() { super.onListenerDisconnected(); - sNotificationListenerInstance = null; + sIsConnected = false; } @Override @@ -164,7 +169,7 @@ public class NotificationListener extends NotificationListenerService { NotificationPostedMsg(StatusBarNotification sbn) { packageUserKey = PackageUserKey.fromNotification(sbn); notificationKey = sbn.getKey(); - shouldBeFilteredOut = shouldBeFilteredOut(sbn.getNotification()); + shouldBeFilteredOut = shouldBeFilteredOut(sbn); } } @@ -188,14 +193,14 @@ public class NotificationListener extends NotificationListenerService { * Filter out notifications that don't have an intent * or are headers for grouped notifications. * - * TODO: use the system concept of a badged notification instead + * @see #shouldBeFilteredOut(StatusBarNotification) */ private List<StatusBarNotification> filterNotifications( StatusBarNotification[] notifications) { if (notifications == null) return null; Set<Integer> removedNotifications = new HashSet<>(); for (int i = 0; i < notifications.length; i++) { - if (shouldBeFilteredOut(notifications[i].getNotification())) { + if (shouldBeFilteredOut(notifications[i])) { removedNotifications.add(i); } } @@ -209,7 +214,14 @@ public class NotificationListener extends NotificationListenerService { return filteredNotifications; } - private boolean shouldBeFilteredOut(Notification notification) { + private boolean shouldBeFilteredOut(StatusBarNotification sbn) { + if (Utilities.isAtLeastO()) { + getCurrentRanking().getRanking(sbn.getKey(), mTempRanking); + if (!mTempRanking.canShowBadge()) { + return true; + } + } + Notification notification = sbn.getNotification(); boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0; return (notification.contentIntent == null || isGroupHeader); } diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java index b3425259b..bb2dac0e1 100644 --- a/src/com/android/launcher3/notification/NotificationMainView.java +++ b/src/com/android/launcher3/notification/NotificationMainView.java @@ -16,38 +16,35 @@ package com.android.launcher3.notification; -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.LinearLayout; +import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.TextView; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; -import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.Themes; /** - * A {@link LinearLayout} that contains a single notification, e.g. icon + title + text. + * A {@link android.widget.FrameLayout} that contains a single notification, + * e.g. icon + title + text. */ -public class NotificationMainView extends LinearLayout implements SwipeHelper.Callback { - - private final ArgbEvaluator mArgbEvaluator = new ArgbEvaluator(); +public class NotificationMainView extends FrameLayout implements SwipeHelper.Callback { private NotificationInfo mNotificationInfo; + private ViewGroup mTextAndBackground; + private int mBackgroundColor; private TextView mTitleView; private TextView mTextView; - private IconPalette mIconPalette; - private ColorDrawable mColorBackground; public NotificationMainView(Context context) { this(context, null, 0); @@ -65,14 +62,15 @@ public class NotificationMainView extends LinearLayout implements SwipeHelper.Ca protected void onFinishInflate() { super.onFinishInflate(); - mTitleView = (TextView) findViewById(R.id.title); - mTextView = (TextView) findViewById(R.id.text); - } - - public void applyColors(IconPalette iconPalette) { - mColorBackground = new ColorDrawable(iconPalette.backgroundColor); - setBackground(mColorBackground); - mIconPalette = iconPalette; + mTextAndBackground = (ViewGroup) findViewById(R.id.text_and_background); + ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground(); + mBackgroundColor = colorBackground.getColor(); + RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf( + Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)), + colorBackground, null); + mTextAndBackground.setBackground(rippleBackground); + mTitleView = (TextView) mTextAndBackground.findViewById(R.id.title); + mTextView = (TextView) mTextAndBackground.findViewById(R.id.text); } public void applyNotificationInfo(NotificationInfo mainNotification, View iconView) { @@ -84,30 +82,18 @@ public class NotificationMainView extends LinearLayout implements SwipeHelper.Ca */ public void applyNotificationInfo(NotificationInfo mainNotification, View iconView, boolean animate) { - if (animate) { - mTitleView.setAlpha(0); - mTextView.setAlpha(0); - mColorBackground.setColor(mIconPalette.secondaryColor); - } mNotificationInfo = mainNotification; mTitleView.setText(mNotificationInfo.title); mTextView.setText(mNotificationInfo.text); - iconView.setBackground(mNotificationInfo.getIconForBackground( - getContext(), mIconPalette.backgroundColor)); + iconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), + mBackgroundColor)); setOnClickListener(mNotificationInfo); setTranslationX(0); // Add a dummy ItemInfo so that logging populates the correct container and item types // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively. setTag(new ItemInfo()); if (animate) { - AnimatorSet animation = LauncherAnimUtils.createAnimatorSet(); - Animator textFade = ObjectAnimator.ofFloat(mTextView, View.ALPHA, 1); - Animator titleFade = ObjectAnimator.ofFloat(mTitleView, View.ALPHA, 1); - ValueAnimator colorChange = ObjectAnimator.ofObject(mColorBackground, "color", - mArgbEvaluator, mIconPalette.secondaryColor, mIconPalette.backgroundColor); - animation.playTogether(textFade, titleFade, colorChange); - animation.setDuration(150); - animation.start(); + ObjectAnimator.ofFloat(mTextAndBackground, ALPHA, 0, 1).setDuration(150).start(); } } diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index d34727c8d..b8d38f587 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -27,14 +27,12 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; -import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.os.Build; import android.os.Handler; import android.os.Looper; -import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; @@ -60,19 +58,18 @@ import com.android.launcher3.Utilities; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; import com.android.launcher3.anim.PropertyListBuilder; +import com.android.launcher3.anim.PropertyResetListener; import com.android.launcher3.badge.BadgeInfo; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; -import com.android.launcher3.dragndrop.DragView; -import com.android.launcher3.graphics.IconPalette; import com.android.launcher3.graphics.TriangleShape; import com.android.launcher3.notification.NotificationItemView; import com.android.launcher3.shortcuts.DeepShortcutView; -import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; +import com.android.launcher3.shortcuts.ShortcutsItemView; import com.android.launcher3.util.PackageUserKey; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -84,18 +81,17 @@ import static com.android.launcher3.userevent.nano.LauncherLogProto.Target; * A container for shortcuts to deep links within apps. */ @TargetApi(Build.VERSION_CODES.N) -public class PopupContainerWithArrow extends AbstractFloatingView - implements View.OnLongClickListener, View.OnTouchListener, DragSource, +public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource, DragController.DragListener { - private final Point mIconShift = new Point(); - private final Point mIconLastTouchPos = new Point(); - protected final Launcher mLauncher; private final int mStartDragThreshold; private LauncherAccessibilityDelegate mAccessibilityDelegate; private final boolean mIsRtl; + public ShortcutsItemView mShortcutsItemView; + private NotificationItemView mNotificationItemView; + protected BubbleTextView mOriginalIcon; private final Rect mTempRect = new Rect(); private PointF mInterceptTouchDown = new PointF(); @@ -177,6 +173,8 @@ public class PopupContainerWithArrow extends AbstractFloatingView boolean reverseOrder = mIsAboveIcon; if (reverseOrder) { removeAllViews(); + mNotificationItemView = null; + mShortcutsItemView = null; itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate); addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1); @@ -184,32 +182,20 @@ public class PopupContainerWithArrow extends AbstractFloatingView orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset); } - List<DeepShortcutView> shortcutViews = new ArrayList<>(); - NotificationItemView notificationView = null; - for (int i = 0; i < getChildCount(); i++) { - View item = getChildAt(i); - switch (itemsToPopulate[i]) { - case SHORTCUT: - if (reverseOrder) { - shortcutViews.add(0, (DeepShortcutView) item); - } else { - shortcutViews.add((DeepShortcutView) item); - } - break; - case NOTIFICATION: - notificationView = (NotificationItemView) item; - IconPalette iconPalette = originalIcon.getIconPalette(); - notificationView.applyColors(iconPalette); - break; - } + ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); + List<DeepShortcutView> shortcutViews = mShortcutsItemView == null + ? Collections.EMPTY_LIST + : mShortcutsItemView.getDeepShortcutViews(reverseOrder); + if (mNotificationItemView != null) { + BadgeInfo badgeInfo = mLauncher.getPopupDataProvider() + .getBadgeInfoForItem(originalItemInfo); + updateNotificationHeader(badgeInfo); } // Add the arrow. mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight); mArrow.setPivotX(arrowWidth / 2); mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight); - PopupItemView firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0); - mArrow.setBackgroundTintList(firstItem.getAttachedArrowColor()); animateOpen(); @@ -220,30 +206,47 @@ public class PopupContainerWithArrow extends AbstractFloatingView // Load the shortcuts on a background thread and update the container as it animates. final Looper workerLooper = LauncherModel.getWorkerLooper(); new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable( - mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()), - this, shortcutIds, shortcutViews, notificationKeys, notificationView)); + mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()), + this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView)); } private void addDummyViews(BubbleTextView originalIcon, - PopupPopulator.Item[] itemsToPopulate, boolean secondaryNotificationViewHasIcons) { + PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) { final Resources res = getResources(); - final int spacing = res.getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); + final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing); final LayoutInflater inflater = mLauncher.getLayoutInflater(); - int numItems = itemsToPopulate.length; + int numItems = itemTypesToPopulate.length; for (int i = 0; i < numItems; i++) { - final PopupItemView item = (PopupItemView) inflater.inflate( - itemsToPopulate[i].layoutId, this, false); - if (itemsToPopulate[i] == PopupPopulator.Item.NOTIFICATION) { - int secondaryHeight = secondaryNotificationViewHasIcons ? - res.getDimensionPixelSize(R.dimen.notification_footer_height) : - res.getDimensionPixelSize(R.dimen.notification_footer_collapsed_height); - item.findViewById(R.id.footer).getLayoutParams().height = secondaryHeight; - } - if (i < numItems - 1) { - ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing; + PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i]; + final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false); + + if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) { + mNotificationItemView = (NotificationItemView) item; + int footerHeight = notificationFooterHasIcons ? + res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0; + item.findViewById(R.id.footer).getLayoutParams().height = footerHeight; } + + boolean itemIsFollowedByDifferentType = i < numItems - 1 + && itemTypesToPopulate[i + 1] != itemTypeToPopulate; + item.setAccessibilityDelegate(mAccessibilityDelegate); - addView(item); + if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) { + if (mShortcutsItemView == null) { + mShortcutsItemView = (ShortcutsItemView) inflater.inflate( + R.layout.shortcuts_item, this, false); + addView(mShortcutsItemView); + } + mShortcutsItemView.addDeepShortcutView((DeepShortcutView) item); + if (itemIsFollowedByDifferentType) { + ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing; + } + } else { + addView(item); + if (itemIsFollowedByDifferentType) { + ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing; + } + } } // TODO: update this, since not all items are shortcuts setContentDescription(getContext().getString(R.string.shortcuts_menu_description, @@ -535,48 +538,26 @@ public class PopupContainerWithArrow extends AbstractFloatingView return true; } - @Override - public boolean onTouch(View v, MotionEvent ev) { - // Touched a shortcut, update where it was touched so we can drag from there on long click. - switch (ev.getAction()) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_MOVE: - mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); - break; + /** + * Updates the notification header to reflect the badge info. Since this can be called + * for any badge info (not necessarily the one associated with this app), we first + * check that the ItemInfo matches the one of this popup. + */ + public void updateNotificationHeader(BadgeInfo badgeInfo, ItemInfo originalItemInfo) { + if (originalItemInfo != mOriginalIcon.getTag()) { + return; } - return false; + updateNotificationHeader(badgeInfo); } - public boolean onLongClick(View v) { - // Return early if this is not initiated from a touch or not the correct view - if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false; - // Return early if global dragging is not enabled - if (!mLauncher.isDraggingEnabled()) return false; - // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts) - if (mLauncher.getDragController().isDragging()) return false; - - // Long clicked on a shortcut. - mDeferContainerRemoval = true; - DeepShortcutView sv = (DeepShortcutView) v.getParent(); - sv.setWillDrawIcon(false); - - // Move the icon to align with the center-top of the touch point - mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; - mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; - - DragView dv = mLauncher.getWorkspace().beginDragShared( - sv.getBubbleText(), this, sv.getFinalInfo(), - new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions()); - dv.animateShift(-mIconShift.x, -mIconShift.y); - - // TODO: support dragging from within folder without having to close it - AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); - return false; + private void updateNotificationHeader(BadgeInfo badgeInfo) { + if (mNotificationItemView != null && badgeInfo != null) { + mNotificationItemView.updateHeader(badgeInfo.getNotificationCount()); + } } public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) { - final NotificationItemView notificationView = (NotificationItemView) findViewById(R.id.notification_view); - if (notificationView == null) { + if (mNotificationItemView == null) { return; } ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); @@ -585,12 +566,11 @@ public class PopupContainerWithArrow extends AbstractFloatingView AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet(); final int duration = getResources().getInteger( R.integer.config_removeNotificationViewDuration); - final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing); - removeNotification.play(animateTranslationYBy(notificationView.getHeight() + spacing, - duration)); - Animator reduceHeight = notificationView.createRemovalAnimation(duration); + final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing); + removeNotification.play(reduceNotificationViewHeight( + mNotificationItemView.getHeight() + spacing, duration, mNotificationItemView)); final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2) - : notificationView; + : mNotificationItemView; if (removeMarginView != null) { ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration); removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -602,19 +582,16 @@ public class PopupContainerWithArrow extends AbstractFloatingView }); removeNotification.play(removeMargin); } - removeNotification.play(reduceHeight); - Animator fade = ObjectAnimator.ofFloat(notificationView, ALPHA, 0) + Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0) .setDuration(duration); fade.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - removeView(notificationView); + removeView(mNotificationItemView); if (getItemCount() == 0) { close(false); return; } - View firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0); - mArrow.setBackgroundTintList(firstItem.getBackgroundTintList()); } }); removeNotification.play(fade); @@ -628,7 +605,7 @@ public class PopupContainerWithArrow extends AbstractFloatingView removeNotification.start(); return; } - notificationView.trimNotifications(badgeInfo.getNotificationKeys()); + mNotificationItemView.trimNotifications(badgeInfo.getNotificationKeys()); } private ObjectAnimator createArrowScaleAnim(float scale) { @@ -636,16 +613,42 @@ public class PopupContainerWithArrow extends AbstractFloatingView mArrow, new PropertyListBuilder().scale(scale).build()); } /** - * Animates the translationY of this container if it is open above the icon. - * If it is below the icon, the container already shifts up when the height - * of a child (e.g. NotificationView) changes, so the translation isn't necessary. + * Animates the height of the notification item and the translationY of other items accordingly. */ - public @Nullable Animator animateTranslationYBy(int translationY, int duration) { + public Animator reduceNotificationViewHeight(int heightToRemove, int duration, + NotificationItemView notificationItem) { + final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove; + AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet(); + animatorSet.play(notificationItem.animateHeightRemoval(heightToRemove)); + PropertyResetListener<View, Float> resetTranslationYListener + = new PropertyResetListener<>(TRANSLATION_Y, 0f); + for (int i = 0; i < getItemCount(); i++) { + final PopupItemView itemView = getItemViewAt(i); + if (!mIsAboveIcon && itemView == notificationItem) { + // The notification view is already in the right place when container is below icon. + continue; + } + ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y, + itemView.getTranslationY() + translateYBy).setDuration(duration); + translateItem.addListener(resetTranslationYListener); + animatorSet.play(translateItem); + } if (mIsAboveIcon) { - return ObjectAnimator.ofFloat(this, TRANSLATION_Y, getTranslationY() + translationY) - .setDuration(duration); + // All the items, including the notification item, translated down, but the + // container itself did not. This means the items would jump back to their + // original translation unless we update the container's translationY here. + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setTranslationY(getTranslationY() + translateYBy); + } + }); } - return null; + return animatorSet; + } + + public Animator reduceNotificationViewHeight(int heightToRemove, int duration) { + return reduceNotificationViewHeight(heightToRemove, duration, mNotificationItemView); } @Override @@ -677,6 +680,7 @@ public class PopupContainerWithArrow extends AbstractFloatingView public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { // Either the original icon or one of the shortcuts was dragged. // Hide the container, but don't remove it yet because that interferes with touch events. + mDeferContainerRemoval = true; animateClose(); } @@ -698,7 +702,6 @@ public class PopupContainerWithArrow extends AbstractFloatingView @Override public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { target.itemType = ItemType.DEEPSHORTCUT; - target.rank = info.rank; targetParent.containerType = ContainerType.DEEPSHORTCUTS; } @@ -740,38 +743,17 @@ public class PopupContainerWithArrow extends AbstractFloatingView for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) { final PopupItemView view = getItemViewAt(i); Animator anim; - if (view.willDrawIcon()) { - anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration); - int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex - : numOpenShortcuts - i - 1; - anim.setStartDelay(stagger * animationIndex); - - Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0); - // Don't start fading until the arrow is gone. - fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration); - fadeAnim.setDuration(duration - arrowScaleDuration); - fadeAnim.setInterpolator(fadeInterpolator); - shortcutAnims.play(fadeAnim); - } else { - // The view is being dragged. Animate it such that it collapses with the drag view - anim = view.collapseToIcon(); - anim.setDuration(DragView.VIEW_ZOOM_DURATION); - - // Scale and translate the view to follow the drag view. - Point iconCenter = view.getIconCenter(); - view.setPivotX(iconCenter.x); - view.setPivotY(iconCenter.y); - - float scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / view.getHeight(); - Animator anim2 = LauncherAnimUtils.ofPropertyValuesHolder(view, - new PropertyListBuilder() - .scale(scale) - .translationX(mIconShift.x) - .translationY(mIconShift.y) - .build()) - .setDuration(DragView.VIEW_ZOOM_DURATION); - shortcutAnims.play(anim2); - } + anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration); + int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex + : numOpenShortcuts - i - 1; + anim.setStartDelay(stagger * animationIndex); + + Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0); + // Don't start fading until the arrow is gone. + fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration); + fadeAnim.setDuration(duration - arrowScaleDuration); + fadeAnim.setInterpolator(fadeInterpolator); + shortcutAnims.play(fadeAnim); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index e314b646b..ee2930f19 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -172,7 +172,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan private boolean updateBadgeIcon(BadgeInfo badgeInfo) { boolean hadNotificationToShow = badgeInfo.hasNotificationToShow(); NotificationInfo notificationInfo = null; - NotificationListener notificationListener = NotificationListener.getInstance(); + NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); if (notificationListener != null && badgeInfo.getNotificationKeys().size() == 1) { StatusBarNotification[] activeNotifications = notificationListener .getActiveNotifications(new String[] {badgeInfo.getNotificationKeys().get(0)}); @@ -222,13 +222,13 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan /** This makes a potentially expensive binder call and should be run on a background thread. */ public List<StatusBarNotification> getStatusBarNotificationsForKeys(String[] notificationKeys) { - NotificationListener notificationListener = NotificationListener.getInstance(); + NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); return notificationListener == null ? Collections.EMPTY_LIST : notificationListener.getNotificationsForKeys(notificationKeys); } public void cancelNotification(String notificationKey) { - NotificationListener notificationListener = NotificationListener.getInstance(); + NotificationListener notificationListener = NotificationListener.getInstanceIfConnected(); if (notificationListener == null) { return; } diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java index 6af6e7d2c..eeece881b 100644 --- a/src/com/android/launcher3/popup/PopupItemView.java +++ b/src/com/android/launcher3/popup/PopupItemView.java @@ -20,9 +20,15 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; -import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; +import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; @@ -30,8 +36,7 @@ import android.widget.FrameLayout; import com.android.launcher3.LogAccelerateInterpolator; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.util.PillRevealOutlineProvider; -import com.android.launcher3.util.PillWidthRevealOutlineProvider; +import com.android.launcher3.anim.PillRevealOutlineProvider; /** * An abstract {@link FrameLayout} that supports animating an item's content @@ -42,11 +47,14 @@ public abstract class PopupItemView extends FrameLayout protected static final Point sTempPoint = new Point(); - private final Rect mPillRect; + protected final Rect mPillRect; private float mOpenAnimationProgress; protected View mIconView; + private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | + Paint.FILTER_BITMAP_FLAG); + public PopupItemView(Context context) { this(context, null, 0); } @@ -73,12 +81,31 @@ public abstract class PopupItemView extends FrameLayout mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); } - protected ColorStateList getAttachedArrowColor() { - return getBackgroundTintList(); + protected void initializeBackgroundClipping(boolean force) { + if (force || mBackgroundClipPaint.getShader() == null) { + mBackgroundClipPaint.setXfermode(null); + mBackgroundClipPaint.setShader(null); + Bitmap backgroundBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), + Bitmap.Config.ALPHA_8); + Canvas canvas = new Canvas(); + canvas.setBitmap(backgroundBitmap); + canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), + getBackgroundRadius(), getBackgroundRadius(), mBackgroundClipPaint); + Shader backgroundClipShader = new BitmapShader(backgroundBitmap, + Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + mBackgroundClipPaint.setShader(backgroundClipShader); + } } - public boolean willDrawIcon() { - return true; + @Override + protected void dispatchDraw(Canvas canvas) { + initializeBackgroundClipping(false /* force */); + int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, + Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); + super.dispatchDraw(canvas); + canvas.drawPaint(mBackgroundClipPaint); + canvas.restoreToCount(saveCount); } /** @@ -126,17 +153,6 @@ public abstract class PopupItemView extends FrameLayout } /** - * Creates an animator which clips the container to form a circle around the icon. - */ - public Animator collapseToIcon() { - int halfHeight = getMeasuredHeight() / 2; - int iconCenterX = getIconCenter().x; - return new PillWidthRevealOutlineProvider(mPillRect, - iconCenterX - halfHeight, iconCenterX + halfHeight) - .createRevealAnimator(this, true); - } - - /** * Returns the position of the center of the icon relative to the container. */ public Point getIconCenter() { @@ -147,6 +163,10 @@ public abstract class PopupItemView extends FrameLayout return sTempPoint; } + protected float getBackgroundRadius() { + return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius); + } + /** * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height. */ @@ -161,10 +181,9 @@ public abstract class PopupItemView extends FrameLayout private final boolean mPivotLeft; private final float mTranslateX; - public ZoomRevealOutlineProvider(int x, int y, Rect pillRect, - View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) { - super(x, y, pillRect, zoomView.getResources().getDimensionPixelSize( - R.dimen.bg_pill_radius)); + public ZoomRevealOutlineProvider(int x, int y, Rect pillRect, PopupItemView translateView, + View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) { + super(x, y, pillRect, translateView.getBackgroundRadius()); mTranslateView = translateView; mZoomView = zoomView; mFullHeight = pillRect.height(); @@ -179,8 +198,10 @@ public abstract class PopupItemView extends FrameLayout public void setProgress(float progress) { super.setProgress(progress); - mZoomView.setScaleX(progress); - mZoomView.setScaleY(progress); + if (mZoomView != null) { + mZoomView.setScaleX(progress); + mZoomView.setScaleY(progress); + } float height = mOutline.height(); mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height)); diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java index d2814ee7b..39c2db2d5 100644 --- a/src/com/android/launcher3/popup/PopupPopulator.java +++ b/src/com/android/launcher3/popup/PopupPopulator.java @@ -196,7 +196,8 @@ public class PopupPopulator { @Override public void run() { - mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer); + mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, + mContainer.mShortcutsItemView); } } diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java index b0482f8b2..3e4cd0192 100644 --- a/src/com/android/launcher3/provider/ImportDataTask.java +++ b/src/com/android/launcher3/provider/ImportDataTask.java @@ -37,6 +37,7 @@ import com.android.launcher3.DefaultLayoutParser; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherFiles; +import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.Settings; @@ -46,7 +47,6 @@ import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.config.ProviderConfig; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.util.LongArrayMap; @@ -112,7 +112,7 @@ public class ImportDataTask { screenOps.add(ContentProviderOperation.newInsert( LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build()); } - mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, screenOps); + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, screenOps); importWorkspaceItems(allScreens.get(0), screenIdMap); GridSizeMigrationTask.markForMigration(mContext, mMaxGridSizeX, mMaxGridSizeY, mHotseatSize); @@ -289,7 +289,7 @@ public class ImportDataTask { } if (insertOperations.size() >= BATCH_INSERT_SIZE) { - mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, insertOperations); insertOperations.clear(); } @@ -300,7 +300,7 @@ public class ImportDataTask { throw new Exception("Insufficient data"); } if (!insertOperations.isEmpty()) { - mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, insertOperations); insertOperations.clear(); } @@ -319,7 +319,7 @@ public class ImportDataTask { mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1; if (!insertOperations.isEmpty()) { - mContext.getContentResolver().applyBatch(ProviderConfig.AUTHORITY, + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, insertOperations); } } diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java index 38a3e1f58..4dc3c1c0d 100644 --- a/src/com/android/launcher3/qsb/QsbContainerView.java +++ b/src/com/android/launcher3/qsb/QsbContainerView.java @@ -40,6 +40,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.config.FeatureFlags; /** * A frame layout which contains a QSB. This internally uses fragment to bind the view, which @@ -89,7 +90,11 @@ public class QsbContainerView extends FrameLayout { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mWrapper = new FrameLayout(getActivity()); - mWrapper.addView(createQsb(mWrapper)); + + // Only add the view when enabled + if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + mWrapper.addView(createQsb(mWrapper)); + } return mWrapper; } @@ -197,6 +202,11 @@ public class QsbContainerView extends FrameLayout { } private void rebindFragment() { + // Exit if the embedded qsb is disabled + if (!FeatureFlags.QSB_ON_FIRST_SCREEN) { + return; + } + if (mWrapper != null && getActivity() != null) { mWrapper.removeAllViews(); mWrapper.addView(createQsb(mWrapper)); diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java index 941391362..df7f6954d 100644 --- a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java +++ b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java @@ -65,7 +65,8 @@ public class DeepShortcutManager { } public static boolean supportsShortcuts(ItemInfo info) { - return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION + && !info.isDisabled(); } public boolean wasLastCallSuccess() { diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java index 2f07c9ac9..47a023e25 100644 --- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java +++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java @@ -17,26 +17,30 @@ package com.android.launcher3.shortcuts; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; +import android.widget.FrameLayout; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.ShortcutInfo; -import com.android.launcher3.popup.PopupContainerWithArrow; -import com.android.launcher3.popup.PopupItemView; +import com.android.launcher3.Utilities; /** * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}. * This lets us animate the DeepShortcutView (icon and text) separately from the background. */ -public class DeepShortcutView extends PopupItemView { +public class DeepShortcutView extends FrameLayout { + + private static final Point sTempPoint = new Point(); private final Rect mPillRect; private DeepShortcutTextView mBubbleText; + private View mIconView; private ShortcutInfo mInfo; private ShortcutInfoCompat mDetail; @@ -59,6 +63,7 @@ public class DeepShortcutView extends PopupItemView { protected void onFinishInflate() { super.onFinishInflate(); mBubbleText = (DeepShortcutTextView) findViewById(R.id.deep_shortcut); + mIconView = findViewById(R.id.icon); } public DeepShortcutTextView getBubbleText() { @@ -73,6 +78,17 @@ public class DeepShortcutView extends PopupItemView { return mIconView.getVisibility() == View.VISIBLE; } + /** + * Returns the position of the center of the icon relative to the container. + */ + public Point getIconCenter() { + sTempPoint.y = sTempPoint.x = getMeasuredHeight() / 2; + if (Utilities.isRtl(getResources())) { + sTempPoint.x = getMeasuredWidth() - sTempPoint.x; + } + return sTempPoint; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); @@ -81,7 +97,7 @@ public class DeepShortcutView extends PopupItemView { /** package private **/ public void applyShortcutInfo(ShortcutInfo info, ShortcutInfoCompat detail, - PopupContainerWithArrow container) { + ShortcutsItemView container) { mInfo = info; mDetail = detail; mBubbleText.applyFromShortcutInfo(info); diff --git a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java new file mode 100644 index 000000000..349c4c946 --- /dev/null +++ b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2017 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.shortcuts; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.content.Context; +import android.graphics.Point; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.anim.PropertyListBuilder; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.dragndrop.DragView; +import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider; +import com.android.launcher3.popup.PopupContainerWithArrow; +import com.android.launcher3.popup.PopupItemView; +import com.android.launcher3.userevent.nano.LauncherLogProto; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link PopupItemView} that contains all of the {@link DeepShortcutView}s for an app. + */ +public class ShortcutsItemView extends PopupItemView implements View.OnLongClickListener, + View.OnTouchListener, LogContainerProvider { + + private Launcher mLauncher; + private LinearLayout mDeepShortcutsLayout; + private final Point mIconShift = new Point(); + private final Point mIconLastTouchPos = new Point(); + + public ShortcutsItemView(Context context) { + this(context, null, 0); + } + + public ShortcutsItemView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ShortcutsItemView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mLauncher = Launcher.getLauncher(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mDeepShortcutsLayout = (LinearLayout) findViewById(R.id.deep_shortcuts); + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + // Touched a shortcut, update where it was touched so we can drag from there on long click. + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if this is not initiated from a touch or not the correct view + if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false; + // Return early if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts) + if (mLauncher.getDragController().isDragging()) return false; + + // Long clicked on a shortcut. + DeepShortcutView sv = (DeepShortcutView) v.getParent(); + sv.setWillDrawIcon(false); + + // Move the icon to align with the center-top of the touch point + mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; + mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; + + DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getBubbleText(), + (PopupContainerWithArrow) getParent(), sv.getFinalInfo(), + new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions()); + dv.animateShift(-mIconShift.x, -mIconShift.y); + + // TODO: support dragging from within folder without having to close it + AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); + return false; + } + + public void addDeepShortcutView(DeepShortcutView deepShortcutView) { + if (getNumDeepShortcuts() > 0) { + getDeepShortcutAt(getNumDeepShortcuts() - 1).findViewById(R.id.divider) + .setVisibility(VISIBLE); + } + mDeepShortcutsLayout.addView(deepShortcutView); + } + + private DeepShortcutView getDeepShortcutAt(int index) { + return (DeepShortcutView) mDeepShortcutsLayout.getChildAt(index); + } + + private int getNumDeepShortcuts() { + return mDeepShortcutsLayout.getChildCount(); + } + + public List<DeepShortcutView> getDeepShortcutViews(boolean reverseOrder) { + int numDeepShortcuts = getNumDeepShortcuts(); + List<DeepShortcutView> deepShortcutViews = new ArrayList<>(numDeepShortcuts); + for (int i = 0; i < numDeepShortcuts; i++) { + DeepShortcutView deepShortcut = getDeepShortcutAt(i); + if (reverseOrder) { + deepShortcutViews.add(0, deepShortcut); + } else { + deepShortcutViews.add(deepShortcut); + } + } + return deepShortcutViews; + } + + @Override + public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) { + AnimatorSet openAnimation = LauncherAnimUtils.createAnimatorSet(); + openAnimation.play(super.createOpenAnimation(isContainerAboveIcon, pivotLeft)); + for (int i = 0; i < getNumDeepShortcuts(); i++) { + View deepShortcutIcon = getDeepShortcutAt(i).getIconView(); + deepShortcutIcon.setScaleX(0); + deepShortcutIcon.setScaleY(0); + openAnimation.play(LauncherAnimUtils.ofPropertyValuesHolder( + deepShortcutIcon, new PropertyListBuilder().scale(1).build())); + } + return openAnimation; + } + + @Override + public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft, + long duration) { + AnimatorSet closeAnimation = LauncherAnimUtils.createAnimatorSet(); + closeAnimation.play(super.createCloseAnimation(isContainerAboveIcon, pivotLeft, duration)); + for (int i = 0; i < getNumDeepShortcuts(); i++) { + View deepShortcutIcon = getDeepShortcutAt(i).getIconView(); + deepShortcutIcon.setScaleX(1); + deepShortcutIcon.setScaleY(1); + closeAnimation.play(LauncherAnimUtils.ofPropertyValuesHolder( + deepShortcutIcon, new PropertyListBuilder().scale(0).build())); + } + return closeAnimation; + } + + @Override + public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target, + LauncherLogProto.Target targetParent) { + target.itemType = LauncherLogProto.ItemType.DEEPSHORTCUT; + target.rank = info.rank; + targetParent.containerType = LauncherLogProto.ContainerType.DEEPSHORTCUTS; + } +} diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java index 6797c7ba3..aedca8db4 100644 --- a/src/com/android/launcher3/testing/LauncherExtension.java +++ b/src/com/android/launcher3/testing/LauncherExtension.java @@ -180,9 +180,6 @@ public class LauncherExtension extends Launcher { } @Override - public UserEventDispatcher getUserEventDispatcher() { return null; } - - @Override public View getQsbBar() { return null; } diff --git a/src/com/android/launcher3/util/LooperExecuter.java b/src/com/android/launcher3/util/LooperExecutor.java index 4db999bce..5b7c20bbf 100644 --- a/src/com/android/launcher3/util/LooperExecuter.java +++ b/src/com/android/launcher3/util/LooperExecutor.java @@ -25,11 +25,11 @@ import java.util.concurrent.TimeUnit; /** * Extension of {@link AbstractExecutorService} which executed on a provided looper. */ -public class LooperExecuter extends AbstractExecutorService { +public class LooperExecutor extends AbstractExecutorService { private final Handler mHandler; - public LooperExecuter(Looper looper) { + public LooperExecutor(Looper looper) { mHandler = new Handler(looper); } diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java new file mode 100644 index 000000000..35cac14e3 --- /dev/null +++ b/src/com/android/launcher3/util/LooperIdleLock.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.util; + +import android.os.Looper; +import android.os.MessageQueue; + +import com.android.launcher3.Utilities; + +/** + * Utility class to block execution until the UI looper is idle. + */ +public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable { + + private final Object mLock; + + private boolean mIsLocked; + + public LooperIdleLock(Object lock, Looper looper) { + mLock = lock; + mIsLocked = true; + if (Utilities.ATLEAST_MARSHMALLOW) { + looper.getQueue().addIdleHandler(this); + } else { + // Looper.myQueue() only gives the current queue. Move the execution to the UI thread + // so that the IdleHandler is attached to the correct message queue. + new LooperExecutor(looper).execute(this); + } + } + + @Override + public void run() { + Looper.myQueue().addIdleHandler(this); + } + + @Override + public boolean queueIdle() { + synchronized (mLock) { + mIsLocked = false; + mLock.notify(); + } + return false; + } + + public boolean awaitLocked(long ms) { + if (mIsLocked) { + try { + // Just in case mFlushingWorkerThread changes but we aren't woken up, + // wait no longer than 1sec at a time + mLock.wait(ms); + } catch (InterruptedException ex) { + // Ignore + } + } + return mIsLocked; + } +} diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java index 577d19f8c..85a000cd8 100644 --- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java +++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java @@ -21,6 +21,7 @@ import android.content.SharedPreferences; import android.content.pm.LauncherActivityInfo; import android.os.Process; import android.os.UserHandle; +import android.support.v4.os.BuildCompat; import com.android.launcher3.AppInfo; import com.android.launcher3.FolderInfo; @@ -31,6 +32,7 @@ import com.android.launcher3.LauncherFiles; import com.android.launcher3.LauncherModel; import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.R; +import com.android.launcher3.SessionCommitReceiver; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.shortcuts.ShortcutInfoCompat; @@ -68,12 +70,15 @@ public class ManagedProfileHeuristic { private final LauncherModel mModel; private final UserHandle mUser; private final IconCache mIconCache; + private final boolean mAddIconsToHomescreen; private ManagedProfileHeuristic(Context context, UserHandle user) { mContext = context; mUser = user; mModel = LauncherAppState.getInstance(context).getModel(); mIconCache = LauncherAppState.getInstance(context).getIconCache(); + mAddIconsToHomescreen = + !BuildCompat.isAtLeastO() || SessionCommitReceiver.isEnabled(context); } public void processPackageRemoved(String[] packages) { @@ -127,7 +132,7 @@ public class ManagedProfileHeuristic { // Do not add shortcuts on the homescreen for the first time. This prevents the launcher // getting filled with the managed user apps, when it start with a fresh DB (or after // a very long time). - if (userAppsExisted && !homescreenApps.isEmpty()) { + if (userAppsExisted && !homescreenApps.isEmpty() && mAddIconsToHomescreen) { mModel.addAndBindAddedWorkspaceItems(new ArrayList<ItemInfo>(homescreenApps)); } } @@ -147,6 +152,13 @@ public class ManagedProfileHeuristic { } // Try to get a work folder. String folderIdKey = USER_FOLDER_ID_PREFIX + mUserManager.getSerialNumberForUser(user); + if (!mAddIconsToHomescreen) { + if (!mPrefs.contains(folderIdKey)) { + // Just mark the folder id preference to avoid new folder creation later. + mPrefs.edit().putLong(folderIdKey, -1).apply(); + } + return; + } if (mPrefs.contains(folderIdKey)) { long folderId = mPrefs.getLong(folderIdKey, 0); final FolderInfo workFolder = mModel.findFolderById(folderId); @@ -163,6 +175,7 @@ public class ManagedProfileHeuristic { @Override public void run() { + workFolder.prepareAutoUpdate(); for (ShortcutInfo info : workFolderApps) { workFolder.add(info, false); } diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index e89fc0ca0..7629f7822 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -40,63 +40,55 @@ import java.util.List; */ public class PackageManagerHelper { - private static final int FLAG_SUSPENDED = 1<<30; - private final Context mContext; private final PackageManager mPm; + private final LauncherAppsCompat mLauncherApps; public PackageManagerHelper(Context context) { mContext = context; mPm = context.getPackageManager(); + mLauncherApps = LauncherAppsCompat.getInstance(context); } /** * Returns true if the app can possibly be on the SDCard. This is just a workaround and doesn't * guarantee that the app is on SD card. */ - public boolean isAppOnSdcard(String packageName) { - return isAppEnabled(packageName, PackageManager.GET_UNINSTALLED_PACKAGES); - } - - public boolean isAppEnabled(String packageName) { - return isAppEnabled(packageName, 0); - } - - public boolean isAppEnabled(String packageName, int flags) { - try { - ApplicationInfo info = mPm.getApplicationInfo(packageName, flags); - return info != null && info.enabled; - } catch (PackageManager.NameNotFoundException e) { - return false; - } + public boolean isAppOnSdcard(String packageName, UserHandle user) { + ApplicationInfo info = mLauncherApps.getApplicationInfo( + packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, user); + return info != null && (info.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } - public boolean isAppSuspended(String packageName) { - try { - ApplicationInfo info = mPm.getApplicationInfo(packageName, 0); - return info != null && isAppSuspended(info); - } catch (PackageManager.NameNotFoundException e) { - return false; - } + /** + * Returns whether the target app is suspended for a given user as per + * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}. + */ + public boolean isAppSuspended(String packageName, UserHandle user) { + ApplicationInfo info = mLauncherApps.getApplicationInfo(packageName, 0, user); + return info != null && isAppSuspended(info); } public boolean isSafeMode() { - return mPm.isSafeMode(); + return mContext.getPackageManager().isSafeMode(); } public Intent getAppLaunchIntent(String pkg, UserHandle user) { - List<LauncherActivityInfo> activities = LauncherAppsCompat.getInstance(mContext) - .getActivityList(pkg, user); + List<LauncherActivityInfo> activities = mLauncherApps.getActivityList(pkg, user); return activities.isEmpty() ? null : AppInfo.makeLaunchIntent(mContext, activities.get(0), user); } + /** + * Returns whether an application is suspended as per + * {@link android.app.admin.DevicePolicyManager#isPackageSuspended}. + */ public static boolean isAppSuspended(ApplicationInfo info) { // The value of FLAG_SUSPENDED was reused by a hidden constant // ApplicationInfo.FLAG_PRIVILEGED prior to N, so only check for suspended flag on N // or later. if (Utilities.ATLEAST_NOUGAT) { - return (info.flags & FLAG_SUSPENDED) != 0; + return (info.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; } else { return false; } diff --git a/src/com/android/launcher3/util/Preconditions.java b/src/com/android/launcher3/util/Preconditions.java index 89353e110..7ab0d3103 100644 --- a/src/com/android/launcher3/util/Preconditions.java +++ b/src/com/android/launcher3/util/Preconditions.java @@ -19,7 +19,7 @@ package com.android.launcher3.util; import android.os.Looper; import com.android.launcher3.LauncherModel; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; /** * A set of utility methods for thread verification. @@ -27,25 +27,25 @@ import com.android.launcher3.config.ProviderConfig; public class Preconditions { public static void assertNotNull(Object o) { - if (ProviderConfig.IS_DOGFOOD_BUILD && o == null) { + if (FeatureFlags.IS_DOGFOOD_BUILD && o == null) { throw new IllegalStateException(); } } public static void assertWorkerThread() { - if (ProviderConfig.IS_DOGFOOD_BUILD && !isSameLooper(LauncherModel.getWorkerLooper())) { + if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(LauncherModel.getWorkerLooper())) { throw new IllegalStateException(); } } public static void assertUIThread() { - if (ProviderConfig.IS_DOGFOOD_BUILD && !isSameLooper(Looper.getMainLooper())) { + if (FeatureFlags.IS_DOGFOOD_BUILD && !isSameLooper(Looper.getMainLooper())) { throw new IllegalStateException(); } } public static void assertNonUiThread() { - if (ProviderConfig.IS_DOGFOOD_BUILD && isSameLooper(Looper.getMainLooper())) { + if (FeatureFlags.IS_DOGFOOD_BUILD && isSameLooper(Looper.getMainLooper())) { throw new IllegalStateException(); } } diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java index 1ff6293a0..5344416ea 100644 --- a/src/com/android/launcher3/util/SQLiteCacheHelper.java +++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java @@ -10,7 +10,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import com.android.launcher3.Utilities; -import com.android.launcher3.config.ProviderConfig; +import com.android.launcher3.config.FeatureFlags; /** * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB. @@ -19,7 +19,7 @@ import com.android.launcher3.config.ProviderConfig; public abstract class SQLiteCacheHelper { private static final String TAG = "SQLiteCacheHelper"; - private static final boolean NO_ICON_CACHE = ProviderConfig.IS_DOGFOOD_BUILD && + private static final boolean NO_ICON_CACHE = FeatureFlags.IS_DOGFOOD_BUILD && Utilities.isPropertyEnabled(LogConfig.MEMORY_ONLY_ICON_CACHE); private final String mTableName; diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java index acd589e20..d86333998 100644 --- a/src/com/android/launcher3/util/Themes.java +++ b/src/com/android/launcher3/util/Themes.java @@ -18,6 +18,8 @@ package com.android.launcher3.util; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.ColorMatrix; import android.view.ContextThemeWrapper; /** @@ -49,4 +51,21 @@ public class Themes { ta.recycle(); return (int) (255 * alpha + 0.5f); } + + /** + * Scales a color matrix such that, when applied to color R G B A, it produces R' G' B' A' where + * R' = r * R + * G' = g * G + * B' = b * B + * A' = a * A + * + * The matrix will, for instance, turn white into r g b a, and black will remain black. + * + * @param color The color r g b a + * @param target The ColorMatrix to scale + */ + public static void setColorScaleOnMatrix(int color, ColorMatrix target) { + target.setScale(Color.red(color) / 255f, Color.green(color) / 255f, + Color.blue(color) / 255f, Color.alpha(color) / 255f); + } } diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java index 9bd288244..4cb6ca831 100644 --- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java +++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java @@ -16,12 +16,10 @@ package com.android.launcher3.util; -import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewTreeObserver.OnDrawListener; -import com.android.launcher3.DeferredHandler; import com.android.launcher3.Launcher; import java.util.ArrayList; @@ -34,7 +32,7 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, OnAttachStateChangeListener { private final ArrayList<Runnable> mTasks = new ArrayList<>(); - private final DeferredHandler mHandler; + private final Executor mExecutor; private Launcher mLauncher; private View mAttachedView; @@ -43,8 +41,8 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, private boolean mLoadAnimationCompleted; private boolean mFirstDrawCompleted; - public ViewOnDrawExecutor(DeferredHandler handler) { - mHandler = handler; + public ViewOnDrawExecutor(Executor executor) { + mExecutor = executor; } public void attachTo(Launcher launcher) { @@ -92,7 +90,7 @@ public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable, // Post the pending tasks after both onDraw and onLoadAnimationCompleted have been called. if (mLoadAnimationCompleted && mFirstDrawCompleted && !mCompleted) { for (final Runnable r : mTasks) { - mHandler.post(r); + mExecutor.execute(r); } markCompleted(); } |