Merge "Default to requesting command type TOGGLE for OverviewCommandHelper" into sc-v2-dev
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index dccc9e1..66ccee6 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -34,6 +34,7 @@
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SCRIM_FOR_APP_LAUNCH;
import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
@@ -508,20 +509,23 @@
launcherAnimator.play(scaleAnim);
});
- int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor);
- int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0);
- int[] colors = isAppOpening
- ? new int[] {scrimColorTrans, scrimColor}
- : new int[] {scrimColor, scrimColorTrans};
- ScrimView scrimView = mLauncher.getScrimView();
- if (scrimView.getBackground() instanceof ColorDrawable) {
- scrimView.setBackgroundColor(colors[0]);
+ final boolean scrimEnabled = ENABLE_SCRIM_FOR_APP_LAUNCH.get();
+ if (scrimEnabled) {
+ int scrimColor = Themes.getAttrColor(mLauncher, R.attr.overviewScrimColor);
+ int scrimColorTrans = ColorUtils.setAlphaComponent(scrimColor, 0);
+ int[] colors = isAppOpening
+ ? new int[]{scrimColorTrans, scrimColor}
+ : new int[]{scrimColor, scrimColorTrans};
+ ScrimView scrimView = mLauncher.getScrimView();
+ if (scrimView.getBackground() instanceof ColorDrawable) {
+ scrimView.setBackgroundColor(colors[0]);
- ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
- colors);
- scrim.setDuration(CONTENT_SCRIM_DURATION);
- scrim.setInterpolator(DEACCEL_1_5);
- launcherAnimator.play(scrim);
+ ObjectAnimator scrim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
+ colors);
+ scrim.setDuration(CONTENT_SCRIM_DURATION);
+ scrim.setInterpolator(DEACCEL_1_5);
+ launcherAnimator.play(scrim);
+ }
}
// Pause page indicator animations as they lead to layer trashing.
@@ -532,7 +536,9 @@
SCALE_PROPERTY.set(view, 1f);
view.setLayerType(View.LAYER_TYPE_NONE, null);
});
- scrimView.setBackgroundColor(Color.TRANSPARENT);
+ if (scrimEnabled) {
+ mLauncher.getScrimView().setBackgroundColor(Color.TRANSPARENT);
+ }
mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
};
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index f4168d9..7d0afe1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -15,27 +15,29 @@
*/
package com.android.launcher3.taskbar;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
-import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_LAUNCHER_STATE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.graphics.Rect;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.systemui.shared.recents.model.ThumbnailData;
/**
@@ -51,12 +53,19 @@
final TaskbarDragLayer mTaskbarDragLayer;
final TaskbarView mTaskbarView;
+ private final AnimatedFloat mIconAlignmentForResumedState =
+ new AnimatedFloat(this::onIconAlignmentRatioChanged);
+ private final AnimatedFloat mIconAlignmentForGestureState =
+ new AnimatedFloat(this::onIconAlignmentRatioChanged);
+
private AnimatedFloat mTaskbarBackgroundAlpha;
private AlphaProperty mIconAlphaForHome;
- private @Nullable Animator mAnimator;
private boolean mIsAnimatingToLauncher;
private TaskbarKeyguardController mKeyguardController;
+ private LauncherState mTargetStateOverride = null;
+ private TaskbarControllers mControllers;
+
public LauncherTaskbarUIController(
BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
mContext = context;
@@ -67,7 +76,6 @@
mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
mHotseatController = new TaskbarHotseatController(
mLauncher, mTaskbarView::updateHotseatItems);
-
}
@Override
@@ -77,21 +85,22 @@
MultiValueAlpha taskbarIconAlpha = taskbarControllers.taskbarViewController
.getTaskbarIconAlpha();
mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
- mTaskbarStateHandler.setAnimationController(taskbarIconAlpha.getProperty(
- ALPHA_INDEX_LAUNCHER_STATE));
+ mControllers = taskbarControllers;
+
mHotseatController.init();
- setTaskbarViewVisible(!mLauncher.hasBeenResumed());
mLauncher.setTaskbarUIController(this);
mKeyguardController = taskbarControllers.taskbarKeyguardController;
+
+ onLauncherResumedOrPaused(mLauncher.hasBeenResumed());
+ mIconAlignmentForResumedState.finishAnimation();
+ onIconAlignmentRatioChanged();
}
@Override
protected void onDestroy() {
- if (mAnimator != null) {
- // End this first, in case it relies on properties that are about to be cleaned up.
- mAnimator.end();
- }
- mTaskbarStateHandler.setAnimationController(null);
+ mIconAlignmentForResumedState.finishAnimation();
+ mIconAlignmentForGestureState.finishAnimation();
+
mHotseatController.cleanup();
setTaskbarViewVisible(true);
mLauncher.getHotseat().setIconsAlpha(1f);
@@ -100,7 +109,7 @@
@Override
protected boolean isTaskbarTouchable() {
- return !mIsAnimatingToLauncher;
+ return !mIsAnimatingToLauncher && mTargetStateOverride == null;
}
@Override
@@ -128,63 +137,82 @@
}
}
- long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
- if (mAnimator != null) {
- mAnimator.cancel();
- }
- if (isResumed) {
- mAnimator = createAnimToLauncher(mLauncher.getStateManager().getState(), duration);
- } else {
- mAnimator = createAnimToApp(duration);
- }
- mAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimator = null;
- }
- });
- mAnimator.start();
+ ObjectAnimator anim = mIconAlignmentForResumedState.animateToValue(
+ getCurrentIconAlignmentRatio(), isResumed ? 1 : 0)
+ .setDuration(QuickstepTransitionManager.CONTENT_ALPHA_DURATION);
+
+ anim.addListener(AnimatorListeners.forEndCallback(() -> mIsAnimatingToLauncher = false));
+ anim.start();
+ mIsAnimatingToLauncher = isResumed;
}
/**
- * Create Taskbar animation when going from an app to Launcher.
+ * Create Taskbar animation when going from an app to Launcher as part of recents transition.
* @param toState If known, the state we will end up in when reaching Launcher.
- * TODO: Move this and createAnimToApp to TaskbarStateHandler using the BACKGROUND state
+ * @param callbacks callbacks to track the recents animation lifecycle. The state change is
+ * automatically reset once the recents animation finishes
*/
- public Animator createAnimToLauncher(@NonNull LauncherState toState, long duration) {
- PendingAnimation anim = new PendingAnimation(duration);
- mTaskbarStateHandler.setState(toState, anim);
-
- anim.setFloat(mTaskbarBackgroundAlpha, AnimatedFloat.VALUE, 0, LINEAR);
- mTaskbarView.alignIconsWithLauncher(mLauncher.getDeviceProfile(), anim);
-
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mIsAnimatingToLauncher = true;
- }
-
+ public Animator createAnimToLauncher(@NonNull LauncherState toState,
+ @NonNull RecentsAnimationCallbacks callbacks,
+ long duration) {
+ ObjectAnimator animator = mIconAlignmentForGestureState
+ .animateToValue(mIconAlignmentForGestureState.value, 1)
+ .setDuration(duration);
+ animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mIsAnimatingToLauncher = false;
- setTaskbarViewVisible(false);
+ mTargetStateOverride = null;
}
- });
- return anim.buildAnim();
- }
-
- private Animator createAnimToApp(long duration) {
- PendingAnimation anim = new PendingAnimation(duration);
- anim.setFloat(mTaskbarBackgroundAlpha, AnimatedFloat.VALUE, 1, LINEAR);
- anim.addListener(AnimatorListeners.forEndCallback(mTaskbarView.resetIconPosition(anim)));
- anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
- setTaskbarViewVisible(true);
+ mTargetStateOverride = toState;
}
});
- return anim.buildAnim();
+ callbacks.addListener(new RecentsAnimationListener() {
+ @Override
+ public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ endGestureStateOverride();
+ }
+
+ @Override
+ public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+ endGestureStateOverride();
+ }
+
+ private void endGestureStateOverride() {
+ callbacks.removeListener(this);
+ mIconAlignmentForGestureState
+ .animateToValue(mIconAlignmentForGestureState.value, 0)
+ .start();
+ }
+ });
+ return animator;
+ }
+
+ private float getCurrentIconAlignmentRatio() {
+ return Math.max(mIconAlignmentForResumedState.value, mIconAlignmentForGestureState.value);
+ }
+
+ private void onIconAlignmentRatioChanged() {
+ if (mControllers == null) {
+ return;
+ }
+ float alignment = getCurrentIconAlignmentRatio();
+ mControllers.taskbarViewController.setLauncherIconAlignment(
+ alignment, mLauncher.getDeviceProfile());
+
+ mTaskbarBackgroundAlpha.updateValue(1 - alignment);
+
+ LauncherState state = mTargetStateOverride != null ? mTargetStateOverride
+ : mLauncher.getStateManager().getState();
+ if ((state.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+ // If the hotseat icons are visible, then switch taskbar in last frame
+ setTaskbarViewVisible(alignment < 1);
+ } else {
+ mLauncher.getHotseat().setIconsAlpha(1);
+ mIconAlphaForHome.setValue(1 - alignment);
+ }
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index d40242c..2facd44 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -105,7 +105,7 @@
return true;
};
- if (mContext.canShowNavButtons()) {
+ if (mContext.isThreeButtonNav()) {
initButtons(mStartContainer, mEndContainer, mControllers.navButtonController);
// Animate taskbar background when IME shows
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 000799b..4142610 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -17,7 +17,6 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
@@ -93,6 +92,7 @@
private final ViewCache mViewCache = new ViewCache();
private final boolean mIsSafeModeEnabled;
+ private boolean mIsDestroyed = false;
public TaskbarActivityContext(Context windowContext, DeviceProfile dp,
TaskbarNavButtonController buttonController) {
@@ -161,8 +161,8 @@
mWindowManager.addView(mDragLayer, mWindowLayoutParams);
}
- public boolean canShowNavButtons() {
- return ENABLE_THREE_BUTTON_TASKBAR && mNavMode == Mode.THREE_BUTTONS;
+ public boolean isThreeButtonNav() {
+ return mNavMode == Mode.THREE_BUTTONS;
}
@Override
@@ -208,13 +208,14 @@
* Called when this instance of taskbar is no longer needed
*/
public void onDestroy() {
+ mIsDestroyed = true;
setUIController(TaskbarUIController.DEFAULT);
mControllers.onDestroy();
mWindowManager.removeViewImmediate(mDragLayer);
}
public void updateSysuiStateFlags(int systemUiStateFlags, boolean forceUpdate) {
- if (!canShowNavButtons()) {
+ if (!isThreeButtonNav()) {
return;
}
mControllers.navbarButtonsViewController.updateStateForSysuiFlags(
@@ -252,7 +253,7 @@
* Updates the TaskbarContainer height (pass deviceProfile.taskbarSize to reset).
*/
public void setTaskbarWindowHeight(int height) {
- if (mWindowLayoutParams.height == height) {
+ if (mWindowLayoutParams.height == height || mIsDestroyed) {
return;
}
if (height != MATCH_PARENT) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 157053e..c48c28b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -61,7 +61,7 @@
*/
public void init() {
navbarButtonsViewController.init(this);
- if (taskbarActivityContext.canShowNavButtons()) {
+ if (taskbarActivityContext.isThreeButtonNav()) {
rotationButtonController.init();
}
taskbarDragLayerController.init(this);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index c684543..ac121ab 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -19,12 +19,15 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
import com.android.systemui.shared.system.ViewTreeObserverWrapper;
@@ -128,4 +131,10 @@
mTaskbarBackgroundPaint.setAlpha((int) (alpha * 255));
invalidate();
}
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
+ return super.dispatchTouchEvent(ev);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 743619b..db5c387 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -93,7 +93,7 @@
*/
public void updateInsetsTouchability(InsetsInfo insetsInfo) {
insetsInfo.touchableRegion.setEmpty();
- if (mActivity.canShowNavButtons()) {
+ if (mActivity.isThreeButtonNav()) {
// Always have nav buttons be touchable
mControllers.navbarButtonsViewController.addVisibleButtonsRegion(
mTaskbarDragLayer, insetsInfo.touchableRegion);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java
index b7799d8..2936bd2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java
@@ -57,7 +57,7 @@
mKeyguardSysuiFlags = interestingKeyguardFlags;
mBouncerShowing = bouncerShowing;
- if (!mContext.canShowNavButtons()) {
+ if (!mContext.isThreeButtonNav()) {
// For gesture nav we don't need to deal with bouncer or showing taskbar when locked
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
index 20d4133..edd2a22 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
@@ -18,45 +18,33 @@
import static com.android.launcher3.LauncherState.TASKBAR;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.SystemUiProxy;
/**
- * StateHandler to animate Taskbar according to Launcher's state machine. Does nothing if Taskbar
- * isn't present (i.e. {@link #setAnimationController} is never called).
+ * StateHandler to animate Taskbar according to Launcher's state machine.
*/
public class TaskbarStateHandler implements StateManager.StateHandler<LauncherState> {
private final BaseQuickstepLauncher mLauncher;
- // Contains Taskbar-related properties we should aniamte. If null, don't do anything.
- private @Nullable MultiValueAlpha.AlphaProperty mTaskbarAlpha = null;
-
private AnimatedFloat mNavbarButtonAlpha = new AnimatedFloat(this::updateNavbarButtonAlpha);
public TaskbarStateHandler(BaseQuickstepLauncher launcher) {
mLauncher = launcher;
}
- public void setAnimationController(MultiValueAlpha.AlphaProperty taskbarAlpha) {
- mTaskbarAlpha = taskbarAlpha;
- // Reapply state.
- setState(mLauncher.getStateManager().getState());
- updateNavbarButtonAlpha();
- }
-
@Override
public void setState(LauncherState state) {
setState(state, PropertySetter.NO_ANIM_PROPERTY_SETTER);
+ // Force update the alpha in case it was not initialized properly
+ updateNavbarButtonAlpha();
}
@Override
@@ -69,12 +57,7 @@
* Sets the provided state
*/
public void setState(LauncherState toState, PropertySetter setter) {
- if (mTaskbarAlpha == null) {
- return;
- }
-
boolean isTaskbarVisible = (toState.getVisibleElements(mLauncher) & TASKBAR) != 0;
- setter.setFloat(mTaskbarAlpha, MultiValueAlpha.VALUE, isTaskbarVisible ? 1f : 0f, LINEAR);
// Make the nav bar visible in states that taskbar isn't visible.
// TODO: We should draw our own handle instead of showing the nav bar.
float navbarButtonAlpha = isTaskbarVisible ? 0f : 1f;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index a952182..7753f96 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -15,11 +15,6 @@
*/
package com.android.launcher3.taskbar;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -34,10 +29,8 @@
import androidx.annotation.Nullable;
import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
-import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -105,51 +98,6 @@
mIconLongClickListener = mControllerCallbacks.getOnLongClickListener();
}
- /**
- * Aligns the icons in the taskbar to that of Launcher.
- */
- public void alignIconsWithLauncher(DeviceProfile launcherDp, PropertySetter setter) {
- Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(getContext());
- float scaleUp = ((float) launcherDp.iconSizePx)
- / mActivityContext.getDeviceProfile().iconSizePx;
- int hotseatCellSize =
- (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right)
- / launcherDp.numShownHotseatIcons;
-
- int offsetY = launcherDp.getTaskbarOffsetY();
- setter.setFloat(this, VIEW_TRANSLATE_Y, -offsetY, LINEAR);
- mActivityContext.setTaskbarWindowHeight(
- mActivityContext.getDeviceProfile().taskbarSize + offsetY);
-
- int count = getChildCount();
- for (int i = 0; i < count; i++) {
- View child = getChildAt(i);
- ItemInfo info = (ItemInfo) child.getTag();
- setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR);
-
- float childCenter = (child.getLeft() + child.getRight()) / 2;
- float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId
- + hotseatCellSize / 2;
- setter.setFloat(child, VIEW_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR);
- }
- }
-
- /**
- * Aligns the icons in the taskbar to that of Launcher.
- * @return a callback to be executed at the end of the setter
- */
- public Runnable resetIconPosition(PropertySetter setter) {
- int count = getChildCount();
- for (int i = 0; i < count; i++) {
- View child = getChildAt(i);
- setter.setFloat(child, SCALE_PROPERTY, 1, LINEAR);
- setter.setFloat(child, VIEW_TRANSLATE_X, 0, LINEAR);
- }
- setter.setFloat(this, VIEW_TRANSLATE_Y, 0, LINEAR);
- return () -> mActivityContext.setTaskbarWindowHeight(
- mActivityContext.getDeviceProfile().taskbarSize);
- }
-
private void removeAndRecycle(View view) {
removeView(view);
view.setOnClickListener(null);
@@ -195,6 +143,7 @@
// so if the info changes we need to reinflate. This should only happen if a new
// folder is dragged to the position that another folder previously existed.
removeAndRecycle(hotseatView);
+ hotseatView = null;
} else {
// View found
break;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 10cc926..c7ac4a4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -15,19 +15,29 @@
*/
package com.android.launcher3.taskbar;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.graphics.Rect;
import android.view.View;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.MultiValueAlpha;
/**
* Handles properties/data collection, then passes the results to TaskbarView to render.
*/
public class TaskbarViewController {
+ private static final Runnable NO_OP = () -> { };
public static final int ALPHA_INDEX_HOME = 0;
- public static final int ALPHA_INDEX_LAUNCHER_STATE = 1;
- public static final int ALPHA_INDEX_IME = 2;
- public static final int ALPHA_INDEX_KEYGUARD = 3;
+ public static final int ALPHA_INDEX_IME = 1;
+ public static final int ALPHA_INDEX_KEYGUARD = 2;
private final TaskbarActivityContext mActivity;
private final TaskbarView mTaskbarView;
@@ -36,10 +46,15 @@
// Initialized in init.
private TaskbarControllers mControllers;
+ // Animation to align icons with Launcher, created lazily. This allows the controller to be
+ // active only during the animation and does not need to worry about layout changes.
+ private AnimatorPlaybackController mIconAlignControllerLazy = null;
+ private Runnable mOnControllerPreCreateCallback = NO_OP;
+
public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
mActivity = activity;
mTaskbarView = taskbarView;
- mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 4);
+ mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 3);
mTaskbarIconAlpha.setUpdateVisibility(true);
}
@@ -72,6 +87,60 @@
}
/**
+ * Sets the taskbar icon alignment relative to Launcher hotseat icons
+ * @param alignmentRatio [0, 1]
+ * 0 => not aligned
+ * 1 => fully aligned
+ */
+ public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
+ if (mIconAlignControllerLazy == null) {
+ mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
+ }
+ mIconAlignControllerLazy.setPlayFraction(alignmentRatio);
+ if (alignmentRatio <= 0 || alignmentRatio >= 1) {
+ // Cleanup lazy controller so that it is created again in next animation
+ mIconAlignControllerLazy = null;
+ }
+ }
+
+ /**
+ * Creates an animation for aligning the taskbar icons with the provided Launcher device profile
+ */
+ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
+ mOnControllerPreCreateCallback.run();
+ PendingAnimation setter = new PendingAnimation(100);
+ Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
+ float scaleUp = ((float) launcherDp.iconSizePx) / mActivity.getDeviceProfile().iconSizePx;
+ int hotseatCellSize =
+ (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right)
+ / launcherDp.numShownHotseatIcons;
+
+ int offsetY = launcherDp.getTaskbarOffsetY();
+ setter.setFloat(mTaskbarView, VIEW_TRANSLATE_Y, -offsetY, LINEAR);
+
+ int collapsedHeight = mActivity.getDeviceProfile().taskbarSize;
+ int expandedHeight = collapsedHeight + offsetY;
+ setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight(
+ anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight));
+
+ int count = mTaskbarView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = mTaskbarView.getChildAt(i);
+ ItemInfo info = (ItemInfo) child.getTag();
+ setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR);
+
+ float childCenter = (child.getLeft() + child.getRight()) / 2;
+ float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId
+ + hotseatCellSize / 2;
+ setter.setFloat(child, VIEW_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR);
+ }
+
+ AnimatorPlaybackController controller = setter.createPlaybackController();
+ mOnControllerPreCreateCallback = () -> controller.setPlayFraction(0);
+ return controller;
+ }
+
+ /**
* Callbacks for {@link TaskbarView} to interact with its controller.
*/
public class TaskbarViewCallbacks {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 0ebaea2..82eaecd 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1107,7 +1107,8 @@
mActivityRestartListener);
mParallelRunningAnim = mActivityInterface.getParallelAnimationToLauncher(
- mGestureState.getEndTarget(), duration);
+ mGestureState.getEndTarget(), duration,
+ mTaskAnimationManager.getCurrentCallbacks());
if (mParallelRunningAnim != null) {
mParallelRunningAnim.start();
}
@@ -1381,11 +1382,17 @@
/**
* Cancels any running animation so that the active target can be overriden by a new swipe
- * handle (in case of quick switch).
+ * handler (in case of quick switch).
*/
private void cancelCurrentAnimation() {
mCanceled = true;
mCurrentShift.cancelAnimation();
+
+ // Cleanup when switching handlers
+ mInputConsumerProxy.unregisterCallback();
+ mActivityInitListener.unregister();
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mActivityRestartListener);
+ mTaskSnapshot = null;
}
private void invalidateHandler() {
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index a45ced4..2699b07 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -346,7 +346,8 @@
* an optional additional animation with the same duration.
*/
public @Nullable Animator getParallelAnimationToLauncher(
- GestureState.GestureEndTarget endTarget, long duration) {
+ GestureState.GestureEndTarget endTarget, long duration,
+ RecentsAnimationCallbacks callbacks) {
if (endTarget == RECENTS) {
ACTIVITY_TYPE activity = getCreatedActivity();
if (activity == null) {
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 30abfbb..799a4c2 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -284,14 +284,15 @@
@Override
public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
- long duration) {
+ long duration, RecentsAnimationCallbacks callbacks) {
LauncherTaskbarUIController uiController = getTaskbarController();
- Animator superAnimator = super.getParallelAnimationToLauncher(endTarget, duration);
- if (uiController == null) {
+ Animator superAnimator = super.getParallelAnimationToLauncher(
+ endTarget, duration, callbacks);
+ if (uiController == null || callbacks == null) {
return superAnimator;
}
LauncherState toState = stateFromGestureEndTarget(endTarget);
- Animator taskbarAnimator = uiController.createAnimToLauncher(toState, duration);
+ Animator taskbarAnimator = uiController.createAnimToLauncher(toState, callbacks, duration);
if (superAnimator == null) {
return taskbarAnimator;
} else {
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 9731bf1..1c178ad 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -28,6 +28,7 @@
import android.os.SystemProperties;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.Utilities;
@@ -261,6 +262,11 @@
mLastAppearedTaskTarget = null;
}
+ @Nullable
+ public RecentsAnimationCallbacks getCurrentCallbacks() {
+ return mCallbacks;
+ }
+
public void dump() {
// TODO
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 8ed6c14..8b64105 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -337,6 +337,7 @@
private float mFullscreenProgress;
private float mGridProgress;
private float mNonGridScale = 1;
+ private float mDismissScale = 1;
private final FullscreenDrawParams mCurrentFullscreenParams;
private final StatefulActivity mActivity;
@@ -916,9 +917,6 @@
if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : right - left);
setPivotY(mSnapshotView.getTop());
- mSnapshotView.setPivotX(
- getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : right - left);
- mSnapshotView.setPivotY(0);
} else {
setPivotX((right - left) * 0.5f);
setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
@@ -950,8 +948,8 @@
}
private void setSnapshotScale(float dismissScale) {
- mSnapshotView.setScaleX(dismissScale);
- mSnapshotView.setScaleY(dismissScale);
+ mDismissScale = dismissScale;
+ applyScale();
}
/**
@@ -969,6 +967,7 @@
private void applyScale() {
float scale = 1;
scale *= getPersistentScale();
+ scale *= mDismissScale;
setScaleX(scale);
setScaleY(scale);
}
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index ea7b7f5..f33abb0 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -114,6 +114,7 @@
@Test
@NavigationModeSwitch(mode = ZERO_BUTTON)
+ @Suppress // until b/190618549 is fixed
public void testSwipeUpFromApp() throws Exception {
try {
// Go to overview once so that all views are initialized and cached
diff --git a/res/drawable/ic_block_no_shadow.xml b/res/drawable/ic_block_no_shadow.xml
index be9aa07..6ac61f4 100644
--- a/res/drawable/ic_block_no_shadow.xml
+++ b/res/drawable/ic_block_no_shadow.xml
@@ -16,8 +16,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
- android:viewportHeight="20.0"
- android:viewportWidth="20.0"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0"
android:tint="?android:attr/textColorPrimary">
<path
android:fillColor="@android:color/white"
diff --git a/res/drawable/ic_corp_off.xml b/res/drawable/ic_corp_off.xml
index 62a9787..117258e 100644
--- a/res/drawable/ic_corp_off.xml
+++ b/res/drawable/ic_corp_off.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 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.
@@ -16,10 +15,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0"
- android:tint="?android:attr/textColorHint" >
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/textColorHint">
<path
- android:pathData="M22 7.95c.05-1.11-.84-2-1.95-1.95H16V3.95c0-1.11-.84-2-1.95-1.95h-4C8.94 1.95 8 2.84 8 3.95v.32l14 14V7.95zM14 6h-4V4h4v2zm7.54 14.28l-7.56-7.56v.01l-1.7-1.7h.01L7.21 5.95 3.25 1.99 1.99 3.27 4.69 6h-.64c-1.11 0-1.99.86-1.99 1.97l-.01 11.02c0 1.11.89 2.01 2 2.01h15.64l2.05 2.02L23 21.75l-1.46-1.47z"
- android:fillColor="@android:color/white"/>
+ android:fillColor="@android:color/white"
+ android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v1.17L10.83,8L20,8v9.17l1.98,1.98c0,-0.05 0.02,-0.1 0.02,-0.16L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2zM19,19L8,8 6,6 2.81,2.81 1.39,4.22 3.3,6.13C2.54,6.41 2.01,7.14 2.01,8L2,19c0,1.11 0.89,2 2,2h14.17l1.61,1.61 1.41,-1.41 -0.37,-0.37L19,19zM4,19L4,8h1.17l11,11L4,19z" />
</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_uninstall_no_shadow.xml b/res/drawable/ic_uninstall_no_shadow.xml
index 14cecac..fbabdd2 100644
--- a/res/drawable/ic_uninstall_no_shadow.xml
+++ b/res/drawable/ic_uninstall_no_shadow.xml
@@ -16,8 +16,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
- android:viewportWidth="20.0"
- android:viewportHeight="20.0"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
android:tint="?android:attr/textColorPrimary" >
<path
android:fillColor="@android:color/white"
diff --git a/res/drawable/work_card_btn.xml b/res/drawable/rounded_action_button.xml
similarity index 84%
rename from res/drawable/work_card_btn.xml
rename to res/drawable/rounded_action_button.xml
index 3c93919..0c8755f 100644
--- a/res/drawable/work_card_btn.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -18,10 +18,10 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <corners android:radius="@dimen/work_edu_card_margin" />
+ <corners android:radius="@dimen/rounded_button_radius" />
<stroke android:width="1dp" android:color="?androidprv:attr/colorAccentPrimaryVariant" />
<padding
- android:left="@dimen/work_fab_radius"
- android:right="@dimen/work_fab_radius" />
+ android:left="@dimen/rounded_button_padding"
+ android:right="@dimen/rounded_button_padding" />
</shape>
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index ffbf962..97feb23 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -47,7 +47,8 @@
android:textColor="?attr/workProfileOverlayTextColor"
android:text="@string/work_profile_edu_accept"
android:textAlignment="center"
- android:background="@drawable/work_card_btn"
+ android:background="@drawable/rounded_action_button"
+
android:textSize="14sp" />
</LinearLayout>
</com.android.launcher3.allapps.WorkEduCard>
\ No newline at end of file
diff --git a/res/layout/work_apps_paused.xml b/res/layout/work_apps_paused.xml
index 14dcb8c..3819256 100644
--- a/res/layout/work_apps_paused.xml
+++ b/res/layout/work_apps_paused.xml
@@ -48,6 +48,6 @@
android:textColor="?attr/workProfileOverlayTextColor"
android:text="@string/work_apps_enable_btn_text"
android:textAlignment="center"
- android:background="@drawable/work_card_btn"
+ android:background="@drawable/rounded_action_button"
android:textSize="14sp" />
</com.android.launcher3.allapps.WorkPausedCard>
\ No newline at end of file
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index 4209192..1771d37 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -25,7 +25,6 @@
android:background="@drawable/work_apps_toggle_background"
android:drawablePadding="16dp"
android:drawableStart="@drawable/ic_corp_off"
- android:elevation="10dp"
android:layout_marginBottom="@dimen/work_fab_margin"
android:layout_marginEnd="@dimen/work_fab_margin"
android:text="@string/work_apps_pause_btn_text" />
\ No newline at end of file
diff --git a/res/values-v28/styles.xml b/res/values-v28/styles.xml
deleted file mode 100644
index 7df9ce5..0000000
--- a/res/values-v28/styles.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright (C) 2019 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.
-*/
--->
-<resources>
- <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" >
- <item name="android:textFontWeight">400</item>
- </style>
-</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 299c80e..04c359e 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -76,6 +76,9 @@
<!-- View IDs to store item highlight information -->
<item type="id" name="view_unhighlight_background" />
+ <!-- view ID used to restore work tab state -->
+ <item type="id" name="work_tab_state_id" />
+
<!-- Menu id for feature flags -->
<item type="id" name="menu_apply_flags" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a94ff06..7fda1e5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -48,7 +48,7 @@
<dimen name="workspace_page_indicator_overlap_workspace">0dp</dimen>
<!-- Drop target bar -->
- <dimen name="dynamic_grid_drop_target_size">48dp</dimen>
+ <dimen name="dynamic_grid_drop_target_size">52dp</dimen>
<dimen name="drop_target_vertical_gap">20dp</dimen>
<!-- App Widget resize frame -->
@@ -98,7 +98,7 @@
<dimen name="all_apps_header_tab_height">48dp</dimen>
<dimen name="all_apps_tabs_indicator_height">2dp</dimen>
<dimen name="all_apps_header_top_padding">36dp</dimen>
- <dimen name="all_apps_header_bottom_padding">16dp</dimen>
+ <dimen name="all_apps_header_bottom_padding">6dp</dimen>
<dimen name="all_apps_work_profile_tab_footer_top_padding">16dp</dimen>
<dimen name="all_apps_work_profile_tab_footer_bottom_padding">20dp</dimen>
<dimen name="all_apps_tabs_vertical_padding">6dp</dimen>
@@ -120,11 +120,16 @@
<!-- Floating action button inside work tab to toggle work profile -->
<dimen name="work_fab_height">48dp</dimen>
<dimen name="work_fab_radius">24dp</dimen>
- <dimen name="work_fab_margin">18dp</dimen>
+ <dimen name="work_fab_margin">16dp</dimen>
<dimen name="work_profile_footer_padding">20dp</dimen>
<dimen name="work_profile_footer_text_size">16sp</dimen>
<dimen name="work_edu_card_margin">16dp</dimen>
+ <!-- rounded button shown inside card views, and snack bars -->
+ <dimen name="rounded_button_height">32dp</dimen>
+ <dimen name="rounded_button_radius">16dp</dimen>
+ <dimen name="rounded_button_padding">8dp</dimen>
+
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9823df3..60c0774 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -190,11 +190,11 @@
<!-- Widgets: -->
<skip />
- <!-- Text to show user in place of a gadget when we can't display it properly -->
- <string name="gadget_error_text">Problem loading widget</string>
+ <!-- Error text that lets a user know that the widget can't load. -->
+ <string name="gadget_error_text">Can\'t load widget</string>
- <!-- Text to show user in place of a gadget when it is not yet initialized. -->
- <string name="gadget_setup_text">Setup</string>
+ <!-- Instructional text to encourage a user to finish setting up the widget. -->
+ <string name="gadget_setup_text">Tap to finish setup</string>
<!-- Text to inform the user that they can't uninstall a system application -->
<string name="uninstall_system_app_text">This is a system app and can\'t be uninstalled.</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 38bc272..8072d0c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -214,7 +214,9 @@
<item name="disabledIconAlpha">.54</item>
</style>
- <style name="BaseIconUnBounded" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
+ <style name="BaseIconRoot" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
+
+ <style name="BaseIconUnBounded" parent="BaseIconRoot">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_gravity">center</item>
@@ -264,9 +266,8 @@
<!-- Drop targets -->
<style name="DropTargetButtonBase" parent="@android:style/TextAppearance.DeviceDefault">
- <item name="android:drawablePadding">7.5dp</item>
- <item name="android:paddingLeft">16dp</item>
- <item name="android:paddingRight">16dp</item>
+ <item name="android:drawablePadding">8dp</item>
+ <item name="android:padding">16dp</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="android:textSize">@dimen/drop_target_text_size</item>
<item name="android:singleLine">true</item>
@@ -276,7 +277,7 @@
<style name="DropTargetButton" parent="DropTargetButtonBase" />
- <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault" />
+ <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
<style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
diff --git a/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
new file mode 100644
index 0000000..c18e26c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/CachingWidgetPreviewLoaderTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.os.UserHandle;
+import android.util.Size;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class CachingWidgetPreviewLoaderTest {
+ private static final Size SIZE_10_10 = new Size(10, 10);
+ private static final Size SIZE_20_20 = new Size(20, 20);
+ private static final String TEST_PACKAGE = "com.example.test";
+ private static final ComponentName TEST_PROVIDER =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider");
+ private static final ComponentName TEST_PROVIDER2 =
+ new ComponentName(TEST_PACKAGE, ".WidgetProvider2");
+ private static final Bitmap BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+ private static final Bitmap BITMAP2 = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888);
+
+
+ @Mock private CancellationSignal mCancellationSignal;
+ @Mock private WidgetPreviewLoader mDelegate;
+ @Mock private IconCache mIconCache;
+ @Mock private DeviceProfile mDeviceProfile;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo;
+ @Mock private LauncherAppWidgetProviderInfo mProviderInfo2;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback;
+ @Mock private WidgetPreviewLoadedCallback mPreviewLoadedCallback2;
+ @Captor private ArgumentCaptor<WidgetPreviewLoadedCallback> mCallbackCaptor;
+
+ private TestActivity mTestActivity;
+ private CachingWidgetPreviewLoader mLoader;
+ private WidgetItem mWidgetItem;
+ private WidgetItem mWidgetItem2;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLoader = new CachingWidgetPreviewLoader(mDelegate);
+
+ mTestActivity = Robolectric.buildActivity(TestActivity.class).setup().get();
+ mTestActivity.setDeviceProfile(mDeviceProfile);
+
+ when(mDelegate.loadPreview(any(), any(), any(), any())).thenReturn(mCancellationSignal);
+
+ mProviderInfo.provider = TEST_PROVIDER;
+ when(mProviderInfo.getProfile()).thenReturn(new UserHandle(0));
+
+ mProviderInfo2.provider = TEST_PROVIDER2;
+ when(mProviderInfo2.getProfile()).thenReturn(new UserHandle(0));
+
+ InvariantDeviceProfile testProfile = new InvariantDeviceProfile();
+ testProfile.numRows = 5;
+ testProfile.numColumns = 5;
+
+ mWidgetItem = new WidgetItem(mProviderInfo, testProfile, mIconCache);
+ mWidgetItem2 = new WidgetItem(mProviderInfo2, testProfile, mIconCache);
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldReturnNull() {
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_notInCache_shouldNotCallDelegate() {
+ mLoader.getPreview(mWidgetItem, SIZE_10_10);
+
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void getPreview_inCache_shouldReturnCachedBitmap() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void getPreview_otherSizeInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isNull();
+ }
+
+ @Test
+ public void getPreview_otherItemInCache_shouldReturnNull() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.getPreview(mWidgetItem2, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void getPreview_shouldStoreMultipleSizesPerItem() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP2);
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_20_20)).isEqualTo(BITMAP2);
+ }
+
+ @Test
+ public void loadPreview_notInCache_shouldStartLoading() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mDelegate).loadPreview(eq(mTestActivity), eq(mWidgetItem), eq(SIZE_10_10), any());
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_shouldCallBack() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ cancellationSignal.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_shouldNotCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+
+ verifyZeroInteractions(mCancellationSignal);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_otherCallListening_loaded_shouldCallBackToNonCancelled() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ cancellationSignal1.cancel();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_thenCancelled_bothCallsCancelled_shouldCancelDelegateRequest() {
+ CancellationSignal cancellationSignal1 =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ CancellationSignal cancellationSignal2 =
+ mLoader.loadPreview(
+ mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ cancellationSignal1.cancel();
+ cancellationSignal2.cancel();
+
+ verify(mCancellationSignal).cancel();
+ verifyZeroInteractions(mPreviewLoadedCallback);
+ verifyZeroInteractions(mPreviewLoadedCallback2);
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isNull();
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldOnlyCallDelegateOnce() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), any());
+ }
+
+ @Test
+ public void loadPreview_multipleCallbacks_shouldForwardResultToEachCallback() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback2);
+
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verify(mPreviewLoadedCallback2).onPreviewLoaded(BITMAP);
+ }
+
+ @Test
+ public void loadPreview_inCache_shouldCallBackImmediately() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ reset(mDelegate);
+
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ verify(mPreviewLoadedCallback).onPreviewLoaded(BITMAP);
+ verifyZeroInteractions(mDelegate);
+ }
+
+ @Test
+ public void loadPreview_thenLoaded_thenCancelled_shouldNotRemovePreviewFromCache() {
+ CancellationSignal cancellationSignal =
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+ loaderCallback.onPreviewLoaded(BITMAP);
+
+ cancellationSignal.cancel();
+
+ assertThat(mLoader.getPreview(mWidgetItem, SIZE_10_10)).isEqualTo(BITMAP);
+ }
+
+ @Test
+ public void isPreviewLoaded_notLoaded_shouldReturnFalse() {
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherSizeLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_otherItemLoaded_shouldReturnFalse() {
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void isPreviewLoaded_loaded_shouldReturnTrue() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_notInCache_shouldBeNoOp() {
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldOnlyRemoveSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isTrue();
+ }
+
+ @Test
+ public void clearPreviews_inCache_otherItems_shouldRemoveAllSpecifiedItems() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_10_10, BITMAP);
+
+ mLoader.clearPreviews(Arrays.asList(mWidgetItem, mWidgetItem2));
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearPreviews_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearPreviews(Collections.singletonList(mWidgetItem));
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ @Test
+ public void clearAll_cacheEmpty_shouldBeNoOp() {
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_shouldRemovePreview() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleSizes_shouldRemoveAllSizes() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_inCache_multipleItems_shouldRemoveAll() {
+ loadPreviewIntoCache(mWidgetItem, SIZE_10_10, BITMAP);
+ loadPreviewIntoCache(mWidgetItem, SIZE_20_20, BITMAP);
+ loadPreviewIntoCache(mWidgetItem2, SIZE_20_20, BITMAP);
+
+ mLoader.clearAll();
+
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_10_10)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem, SIZE_20_20)).isFalse();
+ assertThat(mLoader.isPreviewLoaded(mWidgetItem2, SIZE_20_20)).isFalse();
+ }
+
+ @Test
+ public void clearAll_loading_shouldCancelLoad() {
+ mLoader.loadPreview(mTestActivity, mWidgetItem, SIZE_10_10, mPreviewLoadedCallback);
+
+ mLoader.clearAll();
+
+ verify(mCancellationSignal).cancel();
+ }
+
+ private void loadPreviewIntoCache(WidgetItem widgetItem, Size size, Bitmap bitmap) {
+ reset(mDelegate);
+ mLoader.loadPreview(mTestActivity, widgetItem, size, ignored -> {});
+ verify(mDelegate).loadPreview(any(), any(), any(), mCallbackCaptor.capture());
+ WidgetPreviewLoadedCallback loaderCallback = mCallbackCaptor.getValue();
+
+ loaderCallback.onPreviewLoaded(bitmap);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
index c7286e5..2d87957 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -36,6 +36,10 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
@RunWith(RobolectricTestRunner.class)
public final class LauncherAppWidgetProviderInfoTest {
@@ -234,10 +238,11 @@
Mockito.when(profile.getCellSize()).thenReturn(new Point(CELL_SIZE, CELL_SIZE));
InvariantDeviceProfile idp = new InvariantDeviceProfile();
- idp.supportedProfiles.add(profile);
+ List<DeviceProfile> supportedProfiles = new ArrayList<>(idp.supportedProfiles);
+ supportedProfiles.add(profile);
+ idp.supportedProfiles = Collections.unmodifiableList(supportedProfiles);
idp.numColumns = NUM_OF_COLS;
idp.numRows = NUM_OF_ROWS;
return idp;
}
-
}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
index cc36f63..c946c72 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -207,10 +207,9 @@
// GIVEN the current list has app headers [A, B, E content].
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
List.of(mHeaderA, mHeaderB, mContentE));
- // GIVEN the new list has app headers [A, B, E content].
- List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderB, mContentE);
- // GIVEN the user has interacted with B.
- mHeaderB.setIsWidgetListShown(true);
+ // GIVEN the new list has app headers [A, B, E content] and the user has interacted with B.
+ List<WidgetsListBaseEntry> newList =
+ List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE);
// WHEN computing the list difference.
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
index e1214ff..c730fc0 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -33,13 +33,13 @@
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -64,7 +64,7 @@
private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
@Mock private LayoutInflater mMockLayoutInflater;
- @Mock private WidgetPreviewLoader mMockWidgetCache;
+ @Mock private DatabaseWidgetPreviewLoader mMockWidgetCache;
@Mock private RecyclerView.AdapterDataObserver mListener;
@Mock private IconCache mIconCache;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index 4e2a508..81b0c5f 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
@@ -42,6 +41,7 @@
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
@@ -79,7 +79,7 @@
@Mock
private DeviceProfile mDeviceProfile;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private OnHeaderClickListener mOnHeaderClickListener;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
index d6aea55..a0ba7c3 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java
@@ -34,7 +34,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
@@ -42,6 +41,7 @@
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
@@ -79,7 +79,7 @@
@Mock
private DeviceProfile mDeviceProfile;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private OnHeaderClickListener mOnHeaderClickListener;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 2f1326f..8f9d132 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -38,13 +38,14 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
@@ -85,7 +86,7 @@
@Mock
private IconCache mIconCache;
@Mock
- private WidgetPreviewLoader mWidgetPreviewLoader;
+ private DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@Mock
private DeviceProfile mDeviceProfile;
@@ -113,11 +114,10 @@
/* iconClickListener= */ view -> {},
/* iconLongClickListener= */ view -> false);
mViewHolderBinder = new WidgetsListTableViewHolderBinder(
- mContext,
LayoutInflater.from(mTestActivity),
mOnIconClickListener,
mOnLongClickListener,
- mWidgetPreviewLoader,
+ new CachingWidgetPreviewLoader(mWidgetPreviewLoader),
new WidgetsListDrawableFactory(mTestActivity),
widgetsListAdapter);
}
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 7db34a5..13d5570 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -36,8 +36,6 @@
import android.widget.PopupWindow;
import android.widget.TextView;
-import androidx.appcompat.content.res.AppCompatResources;
-
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -82,6 +80,8 @@
private boolean mAccessibleDrag;
/** An item must be dragged at least this many pixels before this drop target is enabled. */
private final int mDragDistanceThreshold;
+ /** The size of the drawable shown in the drop target. */
+ private final int mDrawableSize;
protected CharSequence mText;
protected ColorStateList mOriginalTextColor;
@@ -103,6 +103,7 @@
Resources resources = getResources();
mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
+ mDrawableSize = resources.getDimensionPixelSize(R.dimen.drop_target_text_size);
}
@Override
@@ -122,13 +123,9 @@
protected void setDrawable(int resId) {
// We do not set the drawable in the xml as that inflates two drawables corresponding to
// drawableLeft and drawableStart.
- if (mTextVisible) {
- setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0, 0, 0);
- mDrawable = getCompoundDrawablesRelative()[0];
- } else {
- setCompoundDrawablesRelativeWithIntrinsicBounds(0, resId, 0, 0);
- mDrawable = getCompoundDrawablesRelative()[1];
- }
+ mDrawable = getContext().getDrawable(resId).mutate();
+ mDrawable.setBounds(0, 0, mDrawableSize, mDrawableSize);
+ setCompoundDrawablesRelative(mDrawable, null, null, null);
}
public void setDropTargetBar(DropTargetBar dropTargetBar) {
@@ -238,11 +235,8 @@
return;
}
final DragLayer dragLayer = mLauncher.getDragLayer();
- final Rect from = new Rect();
- dragLayer.getViewRectRelativeToSelf(d.dragView, from);
-
final Rect to = getIconRect(d);
- final float scale = (float) to.width() / from.width();
+ final float scale = (float) to.width() / d.dragView.getMeasuredWidth();
d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
mDropTargetBar.deferOnDragEnd();
@@ -252,9 +246,9 @@
mLauncher.getStateManager().goToState(NORMAL);
};
- dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
+ dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f,
DRAG_VIEW_DROP_DURATION,
- Interpolators.DEACCEL_2, Interpolators.LINEAR, onAnimationEndRunnable,
+ Interpolators.DEACCEL_2, onAnimationEndRunnable,
DragLayer.ANIMATION_END_DISAPPEAR, null);
}
@@ -329,11 +323,7 @@
if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) {
mTextVisible = isVisible;
setText(newText);
- if (mTextVisible) {
- setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
- } else {
- setCompoundDrawablesRelativeWithIntrinsicBounds(null, mDrawable, null, null);
- }
+ setCompoundDrawablesRelative(mDrawable, null, null, null);
}
}
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 115d3ae..a2bd201 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -138,7 +138,10 @@
public int defaultLayoutId;
int demoModeLayoutId;
- public final List<DeviceProfile> supportedProfiles = new ArrayList<>();
+ /**
+ * An immutable list of supported profiles.
+ */
+ public List<DeviceProfile> supportedProfiles = Collections.EMPTY_LIST;
@Nullable public DevicePaddings devicePaddings;
@@ -313,10 +316,10 @@
// Supported overrides: numRows, numColumns, iconSize
applyPartnerDeviceProfileOverrides(context, metrics);
- supportedProfiles.clear();
+ final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
defaultWallpaperSize = new Point(displayInfo.currentSize);
for (WindowBounds bounds : displayInfo.supportedBounds) {
- supportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
+ localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
.setUseTwoPanels(isSplitDisplay)
.setWindowBounds(bounds).build());
@@ -334,6 +337,7 @@
defaultWallpaperSize.x =
Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
}
+ supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);
ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index b3d096c..3d6be69 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -48,6 +48,7 @@
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState {
@@ -63,7 +64,7 @@
private final LauncherModel mModel;
private final IconProvider mIconProvider;
private final IconCache mIconCache;
- private final WidgetPreviewLoader mWidgetCache;
+ private final DatabaseWidgetPreviewLoader mWidgetCache;
private final InvariantDeviceProfile mInvariantDeviceProfile;
private final RunnableList mOnTerminateCallback = new RunnableList();
@@ -138,7 +139,7 @@
mIconProvider = new IconProvider(context, Themes.isThemedIconEnabled(context));
mIconCache = new IconCache(mContext, mInvariantDeviceProfile,
iconCacheFileName, mIconProvider);
- mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
+ mWidgetCache = new DatabaseWidgetPreviewLoader(mContext, mIconCache);
mModel = new LauncherModel(context, this, mIconCache, new AppFilter(mContext));
mOnTerminateCallback.add(mIconCache::close);
}
@@ -180,7 +181,7 @@
return mModel;
}
- public WidgetPreviewLoader getWidgetCache() {
+ public DatabaseWidgetPreviewLoader getWidgetCache() {
return mWidgetCache;
}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 4579705..9a7ddbc 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -31,7 +31,7 @@
import com.android.launcher3.CellLayout.ContainerType;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
static final String TAG = "ShortcutAndWidgetContainer";
@@ -104,9 +104,9 @@
public void setupLp(View child) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- if (child instanceof LauncherAppWidgetHostView) {
+ if (child instanceof NavigableAppWidgetHostView) {
DeviceProfile profile = mActivity.getDeviceProfile();
- ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
+ ((NavigableAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
@@ -129,8 +129,8 @@
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
final DeviceProfile dp = mActivity.getDeviceProfile();
- if (child instanceof LauncherAppWidgetHostView) {
- ((LauncherAppWidgetHostView) child).getWidgetInset(dp, mTempRect);
+ if (child instanceof NavigableAppWidgetHostView) {
+ ((NavigableAppWidgetHostView) child).getWidgetInset(dp, mTempRect);
lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
dp.appWidgetScale.x, dp.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
@@ -176,16 +176,16 @@
*/
public void layoutChild(View child) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- if (child instanceof LauncherAppWidgetHostView) {
- LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
+ if (child instanceof NavigableAppWidgetHostView) {
+ NavigableAppWidgetHostView nahv = (NavigableAppWidgetHostView) child;
// Scale and center the widget to fit within its cells.
DeviceProfile profile = mActivity.getDeviceProfile();
float scaleX = profile.appWidgetScale.x;
float scaleY = profile.appWidgetScale.y;
- lahv.setScaleToFit(Math.min(scaleX, scaleY));
- lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
+ nahv.setScaleToFit(Math.min(scaleX, scaleY));
+ nahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
-(lp.height - (lp.height * scaleY)) / 2.0f);
}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2ef9e3e..3cabc87 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -123,7 +123,8 @@
public static final boolean ATLEAST_R = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
- public static final boolean ATLEAST_S = BuildCompat.isAtLeastS();
+ public static final boolean ATLEAST_S = BuildCompat.isAtLeastS()
+ || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
/**
* Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index aba8a40..a08fa1f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2732,9 +2732,6 @@
public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
final Runnable onCompleteRunnable, int animationType, final View finalView,
boolean external) {
- Rect from = new Rect();
- mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from);
-
int[] finalPos = new int[2];
float scaleXY[] = new float[2];
boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
@@ -2778,8 +2775,8 @@
}
}
};
- dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0],
- finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
+ dragLayer.animateViewIntoPosition(dragView, finalPos[0],
+ finalPos[1], 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
duration, this);
}
}
@@ -2838,6 +2835,10 @@
removeWorkspaceItem(mDragInfo.cell);
}
} else if (mDragInfo != null) {
+ // When drag is cancelled, reattach content view back to its original parent.
+ if (mDragInfo.cell instanceof LauncherAppWidgetHostView) {
+ d.dragView.detachContentView(/* reattachToPreviousParent= */ true);
+ }
final CellLayout cellLayout = mLauncher.getCellLayout(
mDragInfo.container, mDragInfo.screenId);
if (cellLayout != null) {
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 7003df1..67f2a9e 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -21,6 +21,8 @@
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
@@ -29,6 +31,7 @@
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.Parcelable;
import android.os.Process;
import android.text.Selection;
@@ -80,11 +83,10 @@
Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener,
ScrimView.ScrimDrawingController {
- public static final float PULL_MULTIPLIER = .02f;
- public static final float FLING_VELOCITY_MULTIPLIER = 2000f;
+ private static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
- // Starts the springs after at least 25% of the animation has passed.
- public static final float FLING_ANIMATION_THRESHOLD = 0.25f;
+ public static final float PULL_MULTIPLIER = .02f;
+ public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -132,7 +134,6 @@
private ScrimView mScrimView;
private int mHeaderColor;
-
public AllAppsContainerView(Context context) {
this(context, null);
}
@@ -183,6 +184,23 @@
} catch (Exception e) {
Log.e("AllAppsContainerView", "restoreInstanceState viewId = 0", e);
}
+ Bundle state = (Bundle) sparseArray.get(R.id.work_tab_state_id, null);
+ if (state != null) {
+ int currentPage = state.getInt(BUNDLE_KEY_CURRENT_PAGE, 0);
+ if (currentPage != 0) {
+ rebindAdapters(true);
+ mViewPager.setCurrentPage(currentPage);
+ }
+ }
+ }
+
+
+ @Override
+ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+ super.dispatchSaveInstanceState(container);
+ Bundle state = new Bundle();
+ state.putInt(BUNDLE_KEY_CURRENT_PAGE, getCurrentPage());
+ container.put(R.id.work_tab_state_id, state);
}
/**
@@ -233,7 +251,9 @@
private void resetWorkProfile() {
boolean isEnabled = !mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED);
- mWorkModeSwitch.updateCurrentState(isEnabled);
+ if (mWorkModeSwitch != null) {
+ mWorkModeSwitch.updateCurrentState(isEnabled);
+ }
mWorkAdapterProvider.updateCurrentState(isEnabled);
mAH[AdapterHolder.WORK].applyPadding();
}
@@ -572,6 +592,10 @@
return mViewPager == null ? getActiveRecyclerView() : mViewPager;
}
+ public int getCurrentPage() {
+ return mViewPager != null ? mViewPager.getCurrentPage() : AdapterHolder.MAIN;
+ }
+
/**
* Handles selection on focused view and returns success
*/
@@ -645,20 +669,18 @@
/**
* Adds an update listener to {@param animator} that adds springs to the animation.
*/
- public void addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity) {
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- boolean shouldSpring = true;
-
+ public void addSpringFromFlingUpdateListener(ValueAnimator animator,
+ float velocity /* release velocity */,
+ float progress /* portion of the distance to travel*/) {
+ animator.addListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- if (shouldSpring
- && valueAnimator.getAnimatedFraction() >= FLING_ANIMATION_THRESHOLD) {
- absorbSwipeUpVelocity(Math.max(100, Math.abs(
- Math.round(velocity * FLING_VELOCITY_MULTIPLIER))));
- // calculate the velocity of using the not user controlled interpolator
- // of when the container reach the end.
- shouldSpring = false;
- }
+ public void onAnimationStart(Animator animator) {
+ float distance = (float) ((1 - progress) * getHeight()); // px
+ float settleVelocity = Math.min(0, distance
+ / (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
+ + velocity);
+ absorbSwipeUpVelocity(Math.max(1000, Math.abs(
+ Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
}
});
}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 8ec8269..a0551f0 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -57,6 +57,8 @@
*/
public class AllAppsTransitionController
implements StateHandler<LauncherState>, OnDeviceProfileChangeListener {
+ // This constant should match the second derivative of the animator interpolator.
+ public static final float INTERP_COEFF = 1.7f;
private static final float CONTENT_VISIBLE_MAX_THRESHOLD = 0.5f;
public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 866694f..a800d34 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -15,9 +15,11 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.content.Context;
+import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Build;
import android.os.Process;
@@ -26,12 +28,16 @@
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowInsets;
import android.widget.Button;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.pm.UserCache;
/**
@@ -42,6 +48,10 @@
private Rect mInsets = new Rect();
private boolean mWorkEnabled;
+
+ @Nullable
+ private KeyboardInsetAnimationCallback mKeyboardInsetAnimationCallback;
+
public WorkModeSwitch(Context context) {
this(context, null, 0);
}
@@ -57,7 +67,12 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ setSelected(true);
setOnClickListener(this);
+ if (Utilities.ATLEAST_R) {
+ mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
+ }
}
@Override
@@ -92,6 +107,8 @@
public void onClick(View view) {
if (Utilities.ATLEAST_P) {
setEnabled(false);
+ Launcher.fromContext(getContext()).getStatsLogManager().logger().log(
+ LAUNCHER_TURN_OFF_WORK_APPS_TAP);
UI_HELPER_EXECUTOR.post(() -> setWorkProfileEnabled(getContext(), false));
}
}
@@ -117,4 +134,16 @@
}
return showConfirm;
}
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ if (Utilities.ATLEAST_R) {
+ setTranslationY(0);
+ if (insets.isVisible(WindowInsets.Type.ime())) {
+ Insets keyboardInsets = insets.getInsets(WindowInsets.Type.ime());
+ setTranslationY(mInsets.bottom - keyboardInsets.bottom);
+ }
+ }
+ return insets;
+ }
}
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
index 3955a9a..7908b63 100644
--- a/src/com/android/launcher3/allapps/WorkPausedCard.java
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_ON_WORK_APPS_TAP;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.content.Context;
@@ -61,6 +62,7 @@
public void onClick(View view) {
if (Utilities.ATLEAST_P) {
setEnabled(false);
+ mLauncher.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
UI_HELPER_EXECUTOR.post(() -> WorkModeSwitch.setWorkProfileEnabled(getContext(), true));
}
}
diff --git a/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
new file mode 100644
index 0000000..ef4ada3
--- /dev/null
+++ b/src/com/android/launcher3/anim/KeyboardInsetAnimationCallback.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 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.os.Build;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowInsetsAnimation;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.launcher3.Utilities;
+
+import java.util.List;
+
+/**
+ * Callback that animates views above the IME
+ */
+@RequiresApi(api = Build.VERSION_CODES.R)
+public class KeyboardInsetAnimationCallback extends WindowInsetsAnimation.Callback {
+ private final View mView;
+
+ private float mInitialTranslation;
+ private float mTerminalTranslation;
+
+ public KeyboardInsetAnimationCallback(View view) {
+ super(DISPATCH_MODE_STOP);
+ mView = view;
+ }
+
+ @Override
+ public void onPrepare(WindowInsetsAnimation animation) {
+ mInitialTranslation = mView.getTranslationY();
+ }
+
+
+ @Override
+ public WindowInsets onProgress(WindowInsets windowInsets, List<WindowInsetsAnimation> list) {
+ if (list.size() == 0) {
+ mView.setTranslationY(mInitialTranslation);
+ return windowInsets;
+ }
+ float progress = list.get(0).getInterpolatedFraction();
+
+ mView.setTranslationY(
+ Utilities.mapRange(progress, mInitialTranslation, mTerminalTranslation));
+
+ return windowInsets;
+ }
+
+ @Override
+ public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation,
+ WindowInsetsAnimation.Bounds bounds) {
+ mTerminalTranslation = mView.getTranslationY();
+ mView.setTranslationY(mInitialTranslation);
+ return super.onStart(animation, bounds);
+ }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5e3177a..1c16b1c 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -99,6 +99,9 @@
public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag(
"ENABLE_DEVICE_SEARCH", true, "Allows on device search in all apps");
+ public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(
+ "IME_STICKY_SNACKBAR_EDU", true, "Show sticky IME edu in AllApps");
+
public static final BooleanFlag ENABLE_PEOPLE_TILE_PREVIEW = getDebugFlag(
"ENABLE_PEOPLE_TILE_PREVIEW", false,
"Experimental: Shows conversation shortcuts on home screen as search results");
@@ -220,6 +223,10 @@
"ENABLE_TWO_PANEL_HOME", true,
"Uses two panel on home screen. Only applicable on large screen devices.");
+ public static final BooleanFlag ENABLE_SCRIM_FOR_APP_LAUNCH = getDebugFlag(
+ "ENABLE_SCRIM_FOR_APP_LAUNCH", false,
+ "Enables scrim during app launch animation.");
+
public static final BooleanFlag ENABLE_SPLIT_SELECT = getDebugFlag(
"ENABLE_SPLIT_SELECT", false, "Uses new split screen selection overview UI");
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 011325d..5ee4203 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -17,14 +17,19 @@
package com.android.launcher3.dragndrop;
+import static android.animation.ObjectAnimator.ofFloat;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.Utilities.mapRange;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.animation.TypeEvaluator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -44,10 +49,11 @@
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringProperty;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.graphics.Scrim;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
-import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
@@ -69,11 +75,9 @@
private DragController mDragController;
// Variables relating to animation of views after drop
- private ValueAnimator mDropAnim = null;
+ private Animator mDropAnim = null;
- @Thunk DragView mDropView = null;
- @Thunk int mAnchorViewInitialScrollX = 0;
- @Thunk View mAnchorView = null;
+ private DragView mDropView = null;
private boolean mHoverPointClosesFolder = false;
@@ -220,12 +224,7 @@
public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
int duration) {
- Rect r = new Rect();
- getViewRectRelativeToSelf(dragView, r);
- final int fromX = r.left;
- final int fromY = r.top;
-
- animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
+ animateViewIntoPosition(dragView, pos[0], pos[1], alpha, scaleX, scaleY,
onFinishRunnable, animationEndStyle, duration, null);
}
@@ -241,11 +240,6 @@
parentChildren.measureChild(child);
parentChildren.layoutChild(child);
- Rect dragViewBounds = new Rect();
- getViewRectRelativeToSelf(dragView, dragViewBounds);
- final int fromX = dragViewBounds.left;
- final int fromY = dragViewBounds.top;
-
float coord[] = new float[2];
float childScale = child.getScaleX();
@@ -288,51 +282,50 @@
child.setVisibility(INVISIBLE);
Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE);
- animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
+ animateViewIntoPosition(dragView, toX, toY, 1, toScale, toScale,
onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
}
- public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
- final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
- float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
- int animationEndStyle, int duration, View anchorView) {
- Rect from = new Rect(fromX, fromY, fromX +
- view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
- Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
- animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
- null, null, onCompleteRunnable, animationEndStyle, anchorView);
- }
-
/**
* This method animates a view at the end of a drag and drop animation.
- *
+ */
+ public void animateViewIntoPosition(final DragView view,
+ final int toX, final int toY, float finalAlpha,
+ float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
+ int animationEndStyle, int duration, View anchorView) {
+ Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
+ animateView(view, to, finalAlpha, finalScaleX, finalScaleY, duration,
+ null, onCompleteRunnable, animationEndStyle, anchorView);
+ }
+
+ /**
+ * This method animates a view at the end of a drag and drop animation.
* @param view The view to be animated. This view is drawn directly into DragLayer, and so
* doesn't need to be a child of DragLayer.
- * @param from The initial location of the view. Only the left and top parameters are used.
* @param to The final location of the view. Only the left and top parameters are used. This
- * location doesn't account for scaling, and so should be centered about the desired
- * final location (including scaling).
+* location doesn't account for scaling, and so should be centered about the desired
+* final location (including scaling).
* @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
* @param finalScaleX The final scale of the view. The view is scaled about its center.
* @param finalScaleY The final scale of the view. The view is scaled about its center.
* @param duration The duration of the animation.
* @param motionInterpolator The interpolator to use for the location of the view.
- * @param alphaInterpolator The interpolator to use for the alpha of the view.
* @param onCompleteRunnable Optional runnable to run on animation completion.
* @param animationEndStyle Whether or not to fade out the view once the animation completes.
- * {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
+* {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
* @param anchorView If not null, this represents the view which the animated view stays
- * anchored to in case scrolling is currently taking place. Note: currently this is
- * only used for the X dimension for the case of the workspace.
*/
- public void animateView(final DragView view, final Rect from, final Rect to,
- final float finalAlpha, final float initScaleX, final float initScaleY,
- final float finalScaleX, final float finalScaleY, int duration,
- final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
- final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
+ public void animateView(final DragView view, final Rect to,
+ final float finalAlpha, final float finalScaleX, final float finalScaleY, int duration,
+ final Interpolator motionInterpolator, final Runnable onCompleteRunnable,
+ final int animationEndStyle, View anchorView) {
+ view.cancelAnimation();
+ view.requestLayout();
+
+ final int[] from = getViewLocationRelativeToSelf(view);
// Calculate the duration of the animation based on the object's distance
- final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
+ final float dist = (float) Math.hypot(to.left - from[0], to.top - from[1]);
final Resources res = getResources();
final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
@@ -346,93 +339,45 @@
}
// Fall back to cubic ease out interpolator for the animation if none is specified
- TimeInterpolator interpolator = null;
- if (alphaInterpolator == null || motionInterpolator == null) {
- interpolator = DEACCEL_1_5;
- }
+ TimeInterpolator interpolator =
+ motionInterpolator == null ? DEACCEL_1_5 : motionInterpolator;
// Animate the view
- final float initAlpha = view.getAlpha();
- final float dropViewScale = view.getScaleX();
- AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- final float percent = (Float) animation.getAnimatedValue();
- final int width = view.getMeasuredWidth();
- final int height = view.getMeasuredHeight();
+ PendingAnimation anim = new PendingAnimation(duration);
+ anim.add(ofFloat(view, View.SCALE_X, finalScaleX), interpolator, SpringProperty.DEFAULT);
+ anim.add(ofFloat(view, View.SCALE_Y, finalScaleY), interpolator, SpringProperty.DEFAULT);
+ anim.setViewAlpha(view, finalAlpha, interpolator);
+ anim.setFloat(view, VIEW_TRANSLATE_Y, to.top, interpolator);
- float alphaPercent = alphaInterpolator == null ? percent :
- alphaInterpolator.getInterpolation(percent);
- float motionPercent = motionInterpolator == null ? percent :
- motionInterpolator.getInterpolation(percent);
-
- float initialScaleX = initScaleX * dropViewScale;
- float initialScaleY = initScaleY * dropViewScale;
- float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
- float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
- float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
-
- float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
- float fromTop = from.top + (initialScaleY - 1f) * height / 2;
-
- int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
- int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
-
- int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
- (mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
-
- int xPos = x - mDropView.getScrollX() + anchorAdjust;
- int yPos = y - mDropView.getScrollY();
-
- mDropView.setTranslationX(xPos);
- mDropView.setTranslationY(yPos);
- mDropView.setScaleX(scaleX);
- mDropView.setScaleY(scaleY);
- mDropView.setAlpha(alpha);
- }
- };
- animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
- anchorView);
+ ObjectAnimator xMotion = ofFloat(view, VIEW_TRANSLATE_X, to.left);
+ if (anchorView != null) {
+ final int startScroll = anchorView.getScrollX();
+ TypeEvaluator<Float> evaluator = (f, s, e) -> mapRange(f, s, e)
+ + (anchorView.getScaleX() * (startScroll - anchorView.getScrollX()));
+ xMotion.setEvaluator(evaluator);
+ }
+ anim.add(xMotion, interpolator, SpringProperty.DEFAULT);
+ if (onCompleteRunnable != null) {
+ anim.addListener(forEndCallback(onCompleteRunnable));
+ }
+ playDropAnimation(view, anim.buildAnim(), animationEndStyle);
}
- public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
- TimeInterpolator interpolator, final Runnable onCompleteRunnable,
- final int animationEndStyle, View anchorView) {
+ /**
+ * Runs a previously constructed drop animation
+ */
+ public void playDropAnimation(final DragView view, Animator animator, int animationEndStyle) {
// Clean up the previous animations
if (mDropAnim != null) mDropAnim.cancel();
// Show the drop view if it was previously hidden
mDropView = view;
- mDropView.cancelAnimation();
- mDropView.requestLayout();
-
- // Set the anchor view if the page is scrolling
- if (anchorView != null) {
- mAnchorViewInitialScrollX = anchorView.getScrollX();
- }
- mAnchorView = anchorView;
-
// Create and start the animation
- mDropAnim = new ValueAnimator();
- mDropAnim.setInterpolator(interpolator);
- mDropAnim.setDuration(duration);
- mDropAnim.setFloatValues(0f, 1f);
- mDropAnim.addUpdateListener(updateCb);
- mDropAnim.addListener(new AnimatorListenerAdapter() {
- public void onAnimationEnd(Animator animation) {
- if (onCompleteRunnable != null) {
- onCompleteRunnable.run();
- }
- switch (animationEndStyle) {
- case ANIMATION_END_DISAPPEAR:
- clearAnimatedView();
- break;
- case ANIMATION_END_REMAIN_VISIBLE:
- break;
- }
- mDropAnim = null;
- }
- });
+ mDropAnim = animator;
+ mDropAnim.addListener(forEndCallback(() -> mDropAnim = null));
+ if (animationEndStyle == ANIMATION_END_DISAPPEAR) {
+ mDropAnim.addListener(forEndCallback(this::clearAnimatedView));
+ }
mDropAnim.start();
}
diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
index 3457804..54967a9 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -34,7 +34,6 @@
float totalScale = scaleForItem(curNumItems);
float transX;
float transY;
- float overlayAlpha = 0;
if (index == EXIT_INDEX) {
// 0 1 * <-- Exit position (row 0, col 2)
@@ -55,10 +54,9 @@
transY = mTmpPoint[1];
if (params == null) {
- params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
+ params = new PreviewItemDrawingParams(transX, transY, totalScale);
} else {
params.update(transX, transY, totalScale);
- params.overlayAlpha = overlayAlpha;
}
return params;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 5dcd75a..22bb56c 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -17,7 +17,6 @@
package com.android.launcher3.folder;
import static android.text.TextUtils.isEmpty;
-import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherState.NORMAL;
@@ -39,7 +38,6 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
-import android.os.Build;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
@@ -54,15 +52,12 @@
import android.view.View;
import android.view.ViewDebug;
import android.view.WindowInsets;
-import android.view.WindowInsetsAnimation;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.content.res.ResourcesCompat;
import com.android.launcher3.AbstractFloatingView;
@@ -83,6 +78,7 @@
import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
import com.android.launcher3.accessibility.FolderAccessibilityHelper;
+import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragController.DragListener;
@@ -164,9 +160,9 @@
private final Alarm mReorderAlarm = new Alarm();
private final Alarm mOnExitAlarm = new Alarm();
private final Alarm mOnScrollHintAlarm = new Alarm();
- @Thunk final Alarm mScrollPauseAlarm = new Alarm();
+ final Alarm mScrollPauseAlarm = new Alarm();
- @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
+ final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
private AnimatorSet mCurrentAnimator;
private boolean mIsAnimatingClosed = false;
@@ -182,9 +178,11 @@
private CharSequence mFromTitle;
private FromState mFromLabelState;
- @Thunk FolderIcon mFolderIcon;
+ @Thunk
+ FolderIcon mFolderIcon;
- @Thunk FolderPagedView mContent;
+ @Thunk
+ FolderPagedView mContent;
public FolderNameEditText mFolderName;
private PageIndicatorDots mPageIndicator;
@@ -192,7 +190,8 @@
private int mFooterHeight;
// Cell ranks used for drag and drop
- @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
+ @Thunk
+ int mTargetRank, mPrevTargetRank, mEmptyCellRank;
private Path mClipPath;
@@ -203,7 +202,8 @@
@ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"),
@ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"),
})
- @Thunk int mState = STATE_NONE;
+ @Thunk
+ int mState = STATE_NONE;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mRearrangeOnClose = false;
boolean mItemsInvalidated = false;
@@ -221,12 +221,15 @@
// Folder scrolling
private int mScrollAreaOffset;
- @Thunk int mScrollHintDir = SCROLL_NONE;
- @Thunk int mCurrentScrollDir = SCROLL_NONE;
+ @Thunk
+ int mScrollHintDir = SCROLL_NONE;
+ @Thunk
+ int mCurrentScrollDir = SCROLL_NONE;
private StatsLogManager mStatsLogManager;
- @Nullable private FolderWindowInsetsAnimationCallback mFolderWindowInsetsAnimationCallback;
+ @Nullable
+ private KeyboardInsetAnimationCallback mKeyboardInsetAnimationCallback;
private GradientDrawable mBackground;
@@ -234,7 +237,7 @@
* Used to inflate the Workspace from XML.
*
* @param context The application's context.
- * @param attrs The attributes set containing the Workspace's customization values.
+ * @param attrs The attributes set containing the Workspace's customization values.
*/
public Folder(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -246,7 +249,8 @@
mStatsLogManager = StatsLogManager.newInstance(context);
// We need this view to be focusable in touch mode so that when text editing of the folder
// name is complete, we have something to focus on, thus hiding the cursor and giving
- // reliable behavior when clicking the text field (since it will always gain focus on click).
+ // reliable behavior when clicking the text field (since it will always gain focus on
+ // click).
setFocusableInTouchMode(true);
}
@@ -286,10 +290,8 @@
mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
if (Utilities.ATLEAST_R) {
- mFolderWindowInsetsAnimationCallback =
- new FolderWindowInsetsAnimationCallback(DISPATCH_MODE_STOP, this);
-
- setWindowInsetsAnimationCallback(mFolderWindowInsetsAnimationCallback);
+ mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
}
}
@@ -311,14 +313,14 @@
if (options.isAccessibleDrag) {
mDragController.addDragListener(new AccessibleDragListenerAdapter(
mContent, FolderAccessibilityHelper::new) {
- @Override
- protected void enableAccessibleDrag(boolean enable) {
- super.enableAccessibleDrag(enable);
- mFooter.setImportantForAccessibility(enable
- ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
- });
+ @Override
+ protected void enableAccessibleDrag(boolean enable) {
+ super.enableAccessibleDrag(enable);
+ mFooter.setImportantForAccessibility(enable
+ ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ });
}
mLauncherDelegate.beginDragShared(v, this, options);
@@ -539,7 +541,6 @@
*
* @param activityContext The main ActivityContext in which to inflate this Folder. It must also
* be an instance or ContextWrapper around the Launcher activity context.
- *
* @return A new UserFolder.
*/
@SuppressLint("InflateParams")
@@ -677,6 +678,7 @@
mFolderIcon.setIconVisible(false);
mFolderIcon.drawLeaveBehindIfExists();
}
+
@Override
public void onAnimationEnd(Animator animation) {
mState = STATE_OPEN;
@@ -691,7 +693,7 @@
int footerWidth = mContent.getDesiredWidth()
- mFooter.getPaddingLeft() - mFooter.getPaddingRight();
- float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
+ float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
float translation = (footerWidth - textWidth) / 2;
mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
mPageIndicator.prepareEntryAnimation();
@@ -705,9 +707,9 @@
@Override
public void onAnimationEnd(Animator animation) {
mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
- .translationX(0)
- .setInterpolator(AnimationUtils.loadInterpolator(
- getContext(), android.R.interpolator.fast_out_slow_in));
+ .translationX(0)
+ .setInterpolator(AnimationUtils.loadInterpolator(
+ getContext(), android.R.interpolator.fast_out_slow_in));
mPageIndicator.playEntryAnimation();
if (updateAnimationFlag) {
@@ -794,8 +796,8 @@
@Override
public void onAnimationEnd(Animator animation) {
- if (Utilities.ATLEAST_R && mFolderWindowInsetsAnimationCallback != null) {
- setWindowInsetsAnimationCallback(mFolderWindowInsetsAnimationCallback);
+ if (Utilities.ATLEAST_R && mKeyboardInsetAnimationCallback != null) {
+ setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
}
closeComplete(true);
announceAccessibilityChanges();
@@ -1109,7 +1111,7 @@
sTempRect.set(mActivityContext.getFolderBoundingBox());
int left = Utilities.boundToRange(centeredLeft, sTempRect.left, sTempRect.right - width);
int top = Utilities.boundToRange(centeredTop, sTempRect.top, sTempRect.bottom - height);
- int[] inOutPosition = new int[] {left, top};
+ int[] inOutPosition = new int[]{left, top};
mActivityContext.updateOpenFolderPosition(inOutPosition, sTempRect, width, height);
left = inOutPosition[0];
top = inOutPosition[1];
@@ -1193,7 +1195,7 @@
return mInfo.contents.size();
}
- @Thunk void replaceFolderWithFinalItem() {
+ void replaceFolderWithFinalItem() {
mLauncherDelegate.replaceFolderWithFinalItem(this);
mDestroyed = true;
}
@@ -1352,6 +1354,7 @@
v.setVisibility(INVISIBLE);
}
}
+
public void showItem(WorkspaceItemInfo info) {
View v = getViewForInfo(info);
if (v != null) {
@@ -1646,55 +1649,4 @@
return windowBottomPx - folderBottomPx;
}
-
- /** Callback that animates a folder sliding up above the ime. */
- @RequiresApi(api = Build.VERSION_CODES.R)
- private static class FolderWindowInsetsAnimationCallback
- extends WindowInsetsAnimation.Callback {
-
- private final Folder mFolder;
- float mFolderTranslationStart;
- float mFolderTranslationEnd;
-
- FolderWindowInsetsAnimationCallback(int dispatchMode, Folder folder) {
- super(dispatchMode);
-
- mFolder = folder;
- }
-
- @Override
- public void onPrepare(@NonNull WindowInsetsAnimation animation) {
- mFolderTranslationStart = mFolder.getTranslationY();
- }
-
- @NonNull
- @Override
- public WindowInsetsAnimation.Bounds onStart(
- @NonNull WindowInsetsAnimation animation,
- @NonNull WindowInsetsAnimation.Bounds bounds) {
- mFolderTranslationEnd = mFolder.getTranslationY();
-
- mFolder.setTranslationY(mFolderTranslationStart);
-
- return super.onStart(animation, bounds);
- }
-
- @NonNull
- @Override
- public WindowInsets onProgress(@NonNull WindowInsets windowInsets,
- @NonNull List<WindowInsetsAnimation> list) {
- if (list.size() == 0) {
- mFolder.setTranslationY(0);
-
- return windowInsets;
- }
- float progress = list.get(0).getInterpolatedFraction();
-
- mFolder.setTranslationY(
- Utilities.mapRange(progress, mFolderTranslationStart, mFolderTranslationEnd));
-
- return windowInsets;
- }
-
- }
}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 57b289e..4fe85be 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -79,7 +79,7 @@
private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator;
private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
- private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
private final FolderGridOrganizer mPreviewVerifier;
private ObjectAnimator mBgColorAnimator;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index ed2d0a9..aedb224 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -113,7 +113,7 @@
FolderGridOrganizer mPreviewVerifier;
ClippedFolderIconLayoutRule mPreviewLayoutRule;
private PreviewItemManager mPreviewItemManager;
- private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0);
private List<WorkspaceItemInfo> mCurrentPreviewItems = new ArrayList<>();
boolean mAnimating = false;
@@ -336,8 +336,6 @@
if (animateView != null && mActivity instanceof Launcher) {
final Launcher launcher = (Launcher) mActivity;
DragLayer dragLayer = launcher.getDragLayer();
- Rect from = new Rect();
- dragLayer.getViewRectRelativeToSelf(animateView, from);
Rect to = finalRect;
if (to == null) {
to = new Rect();
@@ -391,7 +389,7 @@
to.offset(center[0] - animateView.getMeasuredWidth() / 2,
center[1] - animateView.getMeasuredHeight() / 2);
- float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f;
+ float finalAlpha = index < MAX_NUM_ITEMS_IN_PREVIEW ? 1f : 0f;
float finalScale = scale * scaleRelativeToDragLayer;
@@ -402,15 +400,19 @@
finalScale *= containerScale;
}
- dragLayer.animateView(animateView, from, to, finalAlpha,
- 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
- Interpolators.DEACCEL_2, Interpolators.ACCEL_2,
- null, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ final int finalIndex = index;
+ dragLayer.animateView(animateView, to, finalAlpha,
+ finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
+ Interpolators.DEACCEL_2,
+ () -> {
+ mPreviewItemManager.hidePreviewItem(finalIndex, false);
+ mFolder.showItem(item);
+ },
+ DragLayer.ANIMATION_END_DISAPPEAR, null);
mFolder.hideItem(item);
if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
- final int finalIndex = index;
FolderNameInfos nameInfos = new FolderNameInfos();
if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
@@ -430,8 +432,6 @@
private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
FolderNameInfos nameInfos, InstanceId instanceId) {
postDelayed(() -> {
- mPreviewItemManager.hidePreviewItem(finalIndex, false);
- mFolder.showItem(item);
setLabelSuggestion(nameInfos, instanceId);
invalidate();
}, DROP_IN_ANIMATION_DURATION);
diff --git a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
index edfd2ba..e20bafb 100644
--- a/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
+++ b/src/com/android/launcher3/folder/FolderPreviewItemAnim.java
@@ -45,7 +45,7 @@
};
private static final PreviewItemDrawingParams sTmpParams =
- new PreviewItemDrawingParams(0, 0, 0, 0);
+ new PreviewItemDrawingParams(0, 0, 0);
private static final float[] sTempParamsArray = new float[3];
private final ObjectAnimator mAnimator;
diff --git a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
index 5746be8..58efdc1 100644
--- a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
+++ b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java
@@ -27,17 +27,15 @@
float transX;
float transY;
float scale;
- float overlayAlpha;
public FolderPreviewItemAnim anim;
public boolean hidden;
public Drawable drawable;
public WorkspaceItemInfo item;
- PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
+ PreviewItemDrawingParams(float transX, float transY, float scale) {
this.transX = transX;
this.transY = transY;
this.scale = scale;
- this.overlayAlpha = overlayAlpha;
}
public void update(float transX, float transY, float scale) {
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index baafbc9..a6674fc 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -260,7 +260,7 @@
params.remove(params.size() - 1);
}
while (items.size() > params.size()) {
- params.add(new PreviewItemDrawingParams(0, 0, 0, 0));
+ params.add(new PreviewItemDrawingParams(0, 0, 0));
}
int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 952b850..a27d5c8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -26,6 +26,7 @@
import android.annotation.TargetApi;
import android.app.Fragment;
+import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
@@ -83,7 +84,10 @@
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.BaseLauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.util.ArrayList;
@@ -201,6 +205,7 @@
private final InsettableFrameLayout mRootView;
private final Hotseat mHotseat;
private final CellLayout mWorkspace;
+ private final AppWidgetHost mAppWidgetHost;
public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp) {
super(context);
@@ -254,6 +259,10 @@
mDp.workspacePadding.top,
mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.bottom);
+
+ mAppWidgetHost = FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()
+ ? new LauncherPreviewAppWidgetHost(context)
+ : null;
}
/** Populate preview and render it. */
@@ -353,9 +362,20 @@
private void inflateAndAddWidgets(
LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
- AppWidgetHostView view = new AppWidgetHostView(mContext);
- view.setAppWidget(-1, providerInfo);
- view.updateAppWidget(null);
+ AppWidgetHostView view;
+ if (FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()) {
+ view = mAppWidgetHost.createView(mContext, info.appWidgetId, providerInfo);
+ } else {
+ view = new NavigableAppWidgetHostView(this) {
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ };
+ view.setAppWidget(-1, providerInfo);
+ view.updateAppWidget(null);
+ }
+
view.setTag(info);
addInScreenFromBind(view, info);
}
@@ -471,4 +491,31 @@
view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
view.layout(0, 0, width, height);
}
+
+ private class LauncherPreviewAppWidgetHost extends AppWidgetHost {
+
+ private LauncherPreviewAppWidgetHost(Context context) {
+ super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID);
+ }
+
+ @Override
+ protected AppWidgetHostView onCreateView(
+ Context context,
+ int appWidgetId,
+ AppWidgetProviderInfo appWidget) {
+ return new LauncherPreviewAppWidgetHostView(LauncherPreviewRenderer.this);
+ }
+ }
+
+ private static class LauncherPreviewAppWidgetHostView extends BaseLauncherAppWidgetHostView {
+
+ private LauncherPreviewAppWidgetHostView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ }
}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 345a2ac..ddff338 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -479,7 +479,13 @@
LAUNCHER_THEMED_ICON_ENABLED(836),
@UiEvent(doc = "User disabled themed icons option in wallpaper & style settings.")
- LAUNCHER_THEMED_ICON_DISABLED(837)
+ LAUNCHER_THEMED_ICON_DISABLED(837),
+
+ @UiEvent(doc = "User tapped on 'Turn on work apps' button in all apps window.")
+ LAUNCHER_TURN_ON_WORK_APPS_TAP(838),
+
+ @UiEvent(doc = "User tapped on 'Turn off work apps' button in all apps window.")
+ LAUNCHER_TURN_OFF_WORK_APPS_TAP(839)
;
// ADD MORE
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 34a21fe..d373cf4 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
+import android.annotation.SuppressLint;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -40,6 +41,7 @@
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
+import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
@@ -50,6 +52,8 @@
import android.util.LongSparseArray;
import android.util.TimingLogger;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
@@ -756,6 +760,9 @@
if (widgetProviderInfo != null
&& (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
|| appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
+ logDeleteWidgetInfo(mApp.getInvariantDeviceProfile(),
+ widgetProviderInfo);
+
// This can happen when display size changes.
c.markDeleted("Widget removed, min sizes not met: "
+ "span=" + appWidgetInfo.spanX + "x"
@@ -982,6 +989,51 @@
&& (provider.provider.getPackageName() != null);
}
+ @SuppressLint("NewApi") // Already added API check.
+ private static void logDeleteWidgetInfo(InvariantDeviceProfile idp,
+ LauncherAppWidgetProviderInfo widgetProviderInfo) {
+ FileLog.d(TAG, "Deleting " + widgetProviderInfo.getComponent()
+ + " due to min size constraint");
+ Point cellSize = new Point();
+ for (DeviceProfile deviceProfile : idp.supportedProfiles) {
+ deviceProfile.getCellSize(cellSize);
+ FileLog.d(TAG, "DeviceProfile available width: " + deviceProfile.availableWidthPx
+ + ", available height: " + deviceProfile.availableHeightPx
+ + ", cellLayoutBorderSpacingPx: " + deviceProfile.cellLayoutBorderSpacingPx
+ + ", cellSize: " + cellSize);
+ }
+
+ StringBuilder widgetDimension = new StringBuilder();
+ widgetDimension.append("Widget dimensions:\n")
+ .append("minResizeWidth: ")
+ .append(widgetProviderInfo.minResizeWidth)
+ .append("\n")
+ .append("minResizeHeight: ")
+ .append(widgetProviderInfo.minResizeHeight)
+ .append("\n")
+ .append("defaultWidth: ")
+ .append(widgetProviderInfo.minWidth)
+ .append("\n")
+ .append("defaultHeight: ")
+ .append(widgetProviderInfo.minHeight)
+ .append("\n");
+ if (Utilities.ATLEAST_S) {
+ widgetDimension.append("targetCellWidth: ")
+ .append(widgetProviderInfo.targetCellWidth)
+ .append("\n")
+ .append("targetCellHeight: ")
+ .append(widgetProviderInfo.targetCellHeight)
+ .append("\n")
+ .append("maxResizeWidth: ")
+ .append(widgetProviderInfo.maxResizeWidth)
+ .append("\n")
+ .append("maxResizeHeight: ")
+ .append(widgetProviderInfo.maxResizeHeight)
+ .append("\n");
+ }
+ FileLog.d(TAG, widgetDimension.toString());
+ }
+
private static void logASplit(final TimingLogger logger, final String label) {
logger.addSplit(label);
if (DEBUG) {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 7bfa3ef..82b0f7c 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -29,6 +29,7 @@
import android.os.UserManager;
import android.util.Log;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -123,6 +124,14 @@
activitiesLists.put(
packages[i], appsList.updatePackage(context, packages[i], mUser));
app.getWidgetCache().removePackage(packages[i], mUser);
+
+ // The update may have changed which shortcuts/widgets are available.
+ // Refresh the widgets for the package if we have an activity running.
+ Launcher launcher = Launcher.ACTIVITY_TRACKER.getCreatedActivity();
+ if (launcher != null) {
+ launcher.refreshAndBindWidgetsForPackageUser(
+ new PackageUserKey(packages[i], mUser));
+ }
}
}
// Since package was just updated, the target must be available now.
@@ -212,7 +221,8 @@
}
if (si.isPromise() && isNewApkAvailable) {
- boolean isTargetValid = true;
+ boolean isTargetValid = !cn.getClassName().equals(
+ IconCache.EMPTY_CLASS_NAME);
if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
List<ShortcutInfo> shortcut =
new ShortcutRequest(context, mUser)
@@ -225,7 +235,7 @@
si.updateFromDeepShortcutInfo(shortcut.get(0), context);
infoUpdated = true;
}
- } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) {
+ } else if (isTargetValid) {
isTargetValid = context.getSystemService(LauncherApps.class)
.isActivityEnabled(cn, mUser);
}
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index 97071bb..7198d54 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -1,7 +1,11 @@
package com.android.launcher3.model;
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
+import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
@@ -59,4 +63,15 @@
}
return false;
}
+
+ /** Returns whether this {@link WidgetItem} has a preview layout that can be used. */
+ @SuppressLint("NewApi") // Already added API check.
+ public boolean hasPreviewLayout() {
+ return ATLEAST_S && widgetInfo != null && widgetInfo.previewLayout != Resources.ID_NULL;
+ }
+
+ /** Returns whether this {@link WidgetItem} is for a shortcut rather than an app widget. */
+ public boolean isShortcut() {
+ return activityInfo != null;
+ }
}
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 2095a0d..cb35f74 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -252,9 +252,9 @@
view.setBackgroundResource(R.drawable.single_item_primary);
} else if (totalVisibleShortcuts > 1) {
if (numVisibleShortcut == 0) {
- view.setBackground(mRoundedTop);
+ view.setBackground(mRoundedTop.getConstantState().newDrawable());
} else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) {
- view.setBackground(mRoundedBottom);
+ view.setBackground(mRoundedBottom.getConstantState().newDrawable());
} else {
view.setBackgroundResource(R.drawable.middle_item_primary);
}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index cc658c9..71d288c 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -18,7 +18,13 @@
import android.content.Context;
import android.content.pm.ShortcutInfo;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
import android.graphics.Point;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.RippleDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -30,16 +36,20 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BubbleTextHolder;
/**
- * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
- * This lets us animate the DeepShortcutView (icon and text) separately from the background.
+ * A {@link android.widget.FrameLayout} that contains an icon and a {@link BubbleTextView} for text.
+ * This lets us animate the child BubbleTextView's background (transparent ripple) separately from
+ * the {@link DeepShortcutView} background color.
*/
public class DeepShortcutView extends FrameLayout implements BubbleTextHolder {
private static final Point sTempPoint = new Point();
+ private final Drawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT);
+
private BubbleTextView mBubbleText;
private View mIconView;
@@ -63,6 +73,43 @@
super.onFinishInflate();
mBubbleText = findViewById(R.id.bubble_text);
mIconView = findViewById(R.id.icon);
+ tryUpdateTextBackground();
+ }
+
+ @Override
+ public void setBackground(Drawable background) {
+ super.setBackground(background);
+ tryUpdateTextBackground();
+ }
+
+ @Override
+ public void setBackgroundResource(int resid) {
+ super.setBackgroundResource(resid);
+ tryUpdateTextBackground();
+ }
+
+ /**
+ * Updates the text background to match the shape of this background (when applicable).
+ */
+ private void tryUpdateTextBackground() {
+ if (!(getBackground() instanceof GradientDrawable) || mBubbleText == null) {
+ return;
+ }
+ GradientDrawable background = (GradientDrawable) getBackground();
+
+ int color = Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight);
+ GradientDrawable backgroundMask = new GradientDrawable();
+ backgroundMask.setColor(color);
+ backgroundMask.setShape(GradientDrawable.RECTANGLE);
+ if (background.getCornerRadii() != null) {
+ backgroundMask.setCornerRadii(background.getCornerRadii());
+ } else {
+ backgroundMask.setCornerRadius(background.getCornerRadius());
+ }
+
+ RippleDrawable drawable = new RippleDrawable(ColorStateList.valueOf(color),
+ mTransparentDrawable, backgroundMask);
+ mBubbleText.setBackground(drawable);
}
@Override
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index a086635..90f37f3 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -335,12 +335,10 @@
mCurrentAnimation.dispatchOnStart();
if (targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
if (mAllAppsOvershootStarted) {
-
mLauncher.getAppsView().onRelease();
mAllAppsOvershootStarted = false;
-
} else {
- mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
+ mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress);
}
}
anim.start();
diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java
index c9aa51c..ac864e9 100644
--- a/src/com/android/launcher3/util/FlingAnimation.java
+++ b/src/com/android/launcher3/util/FlingAnimation.java
@@ -1,12 +1,14 @@
package com.android.launcher3.util;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
@@ -35,7 +37,7 @@
protected final float mUX, mUY;
protected Rect mIconRect;
- protected Rect mFrom;
+ protected RectF mFrom;
protected int mDuration;
protected float mAnimationTimeFraction;
@@ -55,17 +57,17 @@
@Override
public void run() {
mIconRect = mDropTarget.getIconRect(mDragObject);
+ mDragObject.dragView.cancelAnimation();
+ mDragObject.dragView.requestLayout();
// Initiate from
- mFrom = new Rect();
- mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, mFrom);
- float scale = mDragObject.dragView.getScaleX();
- float xOffset = ((scale - 1f) * mDragObject.dragView.getMeasuredWidth()) / 2f;
- float yOffset = ((scale - 1f) * mDragObject.dragView.getMeasuredHeight()) / 2f;
- mFrom.left += xOffset;
- mFrom.right -= xOffset;
- mFrom.top += yOffset;
- mFrom.bottom -= yOffset;
+ Rect from = new Rect();
+ mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, from);
+
+ mFrom = new RectF(from);
+ mFrom.inset(
+ ((1 - mDragObject.dragView.getScaleX()) * from.width()) / 2f,
+ ((1 - mDragObject.dragView.getScaleY()) * from.height()) / 2f);
mDuration = Math.abs(mUY) > Math.abs(mUX) ? initFlingUpDuration() : initFlingLeftDuration();
mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY);
@@ -95,17 +97,15 @@
}
};
- Runnable onAnimationEndRunnable = new Runnable() {
- @Override
- public void run() {
- mLauncher.getStateManager().goToState(NORMAL);
- mDropTarget.completeDrop(mDragObject);
- }
- };
-
mDropTarget.onDrop(mDragObject, mDragOptions);
- mDragLayer.animateView(mDragObject.dragView, this, duration, tInterpolator,
- onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ anim.setDuration(duration).setInterpolator(tInterpolator);
+ anim.addUpdateListener(this);
+ anim.addListener(forEndCallback(() -> {
+ mLauncher.getStateManager().goToState(NORMAL);
+ mDropTarget.completeDrop(mDragObject);
+ }));
+ mDragLayer.playDropAnimation(mDragObject.dragView, anim, DragLayer.ANIMATION_END_DISAPPEAR);
}
/**
@@ -129,7 +129,7 @@
}
double t = (-mUY - Math.sqrt(d)) / mAY;
- float sX = -mFrom.exactCenterX() + mIconRect.exactCenterX();
+ float sX = -mFrom.centerX() + mIconRect.exactCenterX();
// Find horizontal acceleration such that: u*t + a*t*t/2 = s
mAX = (float) ((sX - t * mUX) * 2 / (t * t));
@@ -157,7 +157,7 @@
}
double t = (-mUX - Math.sqrt(d)) / mAX;
- float sY = -mFrom.exactCenterY() + mIconRect.exactCenterY();
+ float sY = -mFrom.centerY() + mIconRect.exactCenterY();
// Find vertical acceleration such that: u*t + a*t*t/2 = s
mAY = (float) ((sY - t * mUY) * 2 / (t * t));
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 2e279fa..c395d6c 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -37,6 +37,7 @@
public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
public static final String SEARCH_EDU_SEEN = "launcher.search_edu";
+ public static final String SEARCH_SNACKBAR_COUNT = "launcher.keyboard_snackbar_count";
/**
* Events that either have happened or have not (booleans).
@@ -47,23 +48,28 @@
SEARCH_EDU_SEEN
})
@Retention(RetentionPolicy.SOURCE)
- public @interface EventBoolKey {}
+ public @interface EventBoolKey {
+ }
/**
* Events that occur multiple times, which we count up to a max defined in {@link #MAX_COUNTS}.
*/
@StringDef(value = {
HOME_BOUNCE_COUNT,
- HOTSEAT_DISCOVERY_TIP_COUNT
+ HOTSEAT_DISCOVERY_TIP_COUNT,
+ SEARCH_SNACKBAR_COUNT
})
@Retention(RetentionPolicy.SOURCE)
- public @interface EventCountKey {}
+ public @interface EventCountKey {
+ }
private static final Map<String, Integer> MAX_COUNTS;
+
static {
Map<String, Integer> maxCounts = new ArrayMap<>(4);
maxCounts.put(HOME_BOUNCE_COUNT, 3);
maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
+ maxCounts.put(SEARCH_SNACKBAR_COUNT, 3);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
}
@@ -103,6 +109,7 @@
/**
* Add 1 to the given event count, if we haven't already reached the max count.
+ *
* @return Whether we have now reached the max count.
*/
public boolean incrementEventCount(@EventCountKey String eventKey) {
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index cb721e6..0f40179 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -21,6 +21,7 @@
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -42,15 +43,30 @@
private static final int MSG_HIDE_KEYBOARD = 1;
private static final int MSG_SET_ORIENTATION = 2;
private static final int MSG_RUN_COMMAND = 3;
+ private static final String STATS_LOGGER_KEY = "STATS_LOGGER_KEY";
@SuppressLint("NewApi")
public static void hideKeyboardAsync(ActivityContext activityContext, IBinder token) {
View root = activityContext.getDragLayer();
- Message.obtain(HANDLER.get(root.getContext()),
- MSG_HIDE_KEYBOARD, token).sendToTarget();
- Launcher.cast(activityContext).getStatsLogManager().logger().log(
- LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+ // Since the launcher context cannot be accessed directly from callback, adding secondary
+ // message to log keyboard close event asynchronously.
+ Bundle mHideKeyboardLoggerMsg = new Bundle();
+ mHideKeyboardLoggerMsg.putParcelable(
+ STATS_LOGGER_KEY,
+ Message.obtain(
+ HANDLER.get(root.getContext()),
+ () -> Launcher.cast(activityContext)
+ .getStatsLogManager()
+ .logger()
+ .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED)
+ )
+ );
+
+ Message mHideKeyboardMsg = Message.obtain(HANDLER.get(root.getContext()), MSG_HIDE_KEYBOARD,
+ token);
+ mHideKeyboardMsg.setData(mHideKeyboardLoggerMsg);
+ mHideKeyboardMsg.sendToTarget();
}
public static void setOrientationAsync(Activity activity, int orientation) {
@@ -81,7 +97,11 @@
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_HIDE_KEYBOARD:
- mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0);
+ if (mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0)) {
+ // log keyboard close event only when keyboard is actually closed
+ ((Message) message.getData().getParcelable(STATS_LOGGER_KEY))
+ .sendToTarget();
+ }
return true;
case MSG_SET_ORIENTATION:
((Activity) message.obj).setRequestedOrientation(message.arg1);
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 01c0b56..76dfb3c 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -430,18 +430,20 @@
}
public void getViewRectRelativeToSelf(View v, Rect r) {
+ int[] loc = getViewLocationRelativeToSelf(v);
+ r.set(loc[0], loc[1], loc[0] + v.getMeasuredWidth(), loc[1] + v.getMeasuredHeight());
+ }
+
+ protected int[] getViewLocationRelativeToSelf(View v) {
int[] loc = new int[2];
getLocationInWindow(loc);
int x = loc[0];
int y = loc[1];
v.getLocationInWindow(loc);
- int vX = loc[0];
- int vY = loc[1];
-
- int left = vX - x;
- int top = vY - y;
- r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
+ loc[0] -= x;
+ loc[1] -= y;
+ return loc;
}
@Override
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index 8e3ac20..8f814a1 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -108,8 +108,6 @@
switch (direction) {
case DIRECTION_TOP:
return new EdgeEffectProxy(getContext(), mEdgeGlowTop);
- case DIRECTION_BOTTOM:
- return new EdgeEffectProxy(getContext(), mEdgeGlowBottom);
}
return super.createEdgeEffect(view, direction);
}
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
new file mode 100644
index 0000000..2742882
--- /dev/null
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Launcher AppWidgetHostView with support for rounded corners and a fallback View.
+ */
+public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView {
+
+ protected final LayoutInflater mInflater;
+
+ private final Rect mEnforcedRectangle = new Rect();
+ private final float mEnforcedCornerRadius;
+ private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
+ outline.setEmpty();
+ } else {
+ outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
+ }
+ }
+ };
+
+ public BaseLauncherAppWidgetHostView(Context context) {
+ super(context);
+
+ setExecutor(Executors.THREAD_POOL_EXECUTOR);
+
+ mInflater = LayoutInflater.from(context);
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ }
+
+ @Override
+ protected View getErrorView() {
+ return mInflater.inflate(R.layout.appwidget_error, this, false);
+ }
+
+ /**
+ * Fall back to error layout instead of showing widget.
+ */
+ public void switchToErrorView() {
+ // Update the widget with 0 Layout id, to reset the view to error view.
+ updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ try {
+ super.onLayout(changed, left, top, right, bottom);
+ } catch (final RuntimeException e) {
+ post(this::switchToErrorView);
+ }
+
+ enforceRoundedCorners();
+ }
+
+ @UiThread
+ private void resetRoundedCorners() {
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ setClipToOutline(false);
+ }
+
+ @UiThread
+ private void enforceRoundedCorners() {
+ if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+ resetRoundedCorners();
+ return;
+ }
+ View background = RoundedCornerEnforcement.findBackground(this);
+ if (background == null
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ resetRoundedCorners();
+ return;
+ }
+ RoundedCornerEnforcement.computeRoundedRectangle(this,
+ background,
+ mEnforcedRectangle);
+ setOutlineProvider(mCornerRadiusEnforcementOutline);
+ setClipToOutline(true);
+ }
+
+ /** Returns the corner radius currently enforced, in pixels. */
+ public float getEnforcedCornerRadius() {
+ return mEnforcedCornerRadius;
+ }
+
+ /** Returns true if the corner radius are enforced for this App Widget. */
+ public boolean hasEnforcedCornerRadius() {
+ return getClipToOutline();
+ }
+}
diff --git a/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
new file mode 100644
index 0000000..afceadd
--- /dev/null
+++ b/src/com/android/launcher3/widget/CachingWidgetPreviewLoader.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Wrapper around {@link DatabaseWidgetPreviewLoader} that contains caching logic. */
+public class CachingWidgetPreviewLoader implements WidgetPreviewLoader {
+
+ @NonNull private final WidgetPreviewLoader mDelegate;
+ @NonNull private final Map<ComponentKey, Map<Size, CacheResult>> mCache = new ArrayMap<>();
+
+ public CachingWidgetPreviewLoader(@NonNull WidgetPreviewLoader delegate) {
+ mDelegate = delegate;
+ }
+
+ /** Returns whether the preview is loaded for the item and size. */
+ public boolean isPreviewLoaded(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ return getPreview(item, previewSize) != null;
+ }
+
+ /** Returns the cached preview for the item and size, or null if there is none. */
+ @Nullable
+ public Bitmap getPreview(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ return ((CacheResult.Loaded) cacheResult).mBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ @NonNull
+ private CacheResult getCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.get(toComponentKey(item));
+ if (cacheResults == null) {
+ return CacheResult.MISS;
+ }
+
+ return cacheResults.getOrDefault(previewSize, CacheResult.MISS);
+ }
+ }
+
+ /**
+ * Puts the result in the cache for the item and size. Returns the value previously in the
+ * cache, or null if there was none.
+ */
+ @Nullable
+ private CacheResult putCacheResult(
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @Nullable CacheResult cacheResult) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ CacheResult previous;
+ if (cacheResult == null) {
+ previous = cacheResults.remove(previewSize);
+ if (cacheResults.isEmpty()) {
+ mCache.remove(key);
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ } else {
+ previous = cacheResults.put(previewSize, cacheResult);
+ mCache.put(key, cacheResults);
+ }
+ return previous;
+ }
+ }
+
+ private void removeCacheResult(@NonNull WidgetItem item, @NonNull Size previewSize) {
+ ComponentKey key = toComponentKey(item);
+ synchronized (mCache) {
+ Map<Size, CacheResult> cacheResults = mCache.getOrDefault(key, new ArrayMap<>());
+ cacheResults.remove(previewSize);
+ mCache.put(key, cacheResults);
+ }
+ }
+
+ /**
+ * Gets the preview for the widget item and size, using the value in the cache if stored.
+ *
+ * @return a {@link CancellationSignal}, which can cancel the request before it loads
+ */
+ @Override
+ @UiThread
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity, @NonNull WidgetItem item, @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ CancellationSignal signal = new CancellationSignal();
+ signal.setOnCancelListener(() -> {
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (!(cacheResult instanceof CacheResult.Loading)) {
+ // If the key isn't actively loading, then this is a no-op. Cancelling loading
+ // shouldn't clear the cache if we've already loaded.
+ return;
+ }
+
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ CacheResult.Loading updated = prev.withoutCallback(callback);
+
+ if (updated.mCallbacks.isEmpty()) {
+ // If the last callback was removed, then cancel the underlying request in the
+ // delegate.
+ prev.mCancellationSignal.cancel();
+ removeCacheResult(item, previewSize);
+ } else {
+ // If there are other callbacks still active, then don't cancel the delegate's
+ // request, just remove this callback from the set.
+ putCacheResult(item, previewSize, updated);
+ }
+ }
+ });
+
+ synchronized (mCache) {
+ CacheResult cacheResult = getCacheResult(item, previewSize);
+ if (cacheResult instanceof CacheResult.Loaded) {
+ // If the bitmap is already present in the cache, invoke the callback immediately.
+ callback.onPreviewLoaded(((CacheResult.Loaded) cacheResult).mBitmap);
+ return signal;
+ }
+
+ if (cacheResult instanceof CacheResult.Loading) {
+ // If we're already loading the preview for this key, then just add the callback
+ // to the set we'll call after it loads.
+ CacheResult.Loading prev = (CacheResult.Loading) cacheResult;
+ putCacheResult(item, previewSize, prev.withCallback(callback));
+ return signal;
+ }
+
+ CancellationSignal delegateCancellationSignal =
+ mDelegate.loadPreview(
+ activity,
+ item,
+ previewSize,
+ preview -> {
+ CacheResult prev;
+ synchronized (mCache) {
+ prev = putCacheResult(
+ item, previewSize, new CacheResult.Loaded(preview));
+ }
+ if (prev instanceof CacheResult.Loading) {
+ // Notify each stored callback that the preview has loaded.
+ ((CacheResult.Loading) prev).mCallbacks
+ .forEach(c -> c.onPreviewLoaded(preview));
+ } else {
+ // If there isn't a loading object in the cache, then we were
+ // notified before adding this signal to the cache. Just
+ // call back to the provided callback, there can't be others.
+ callback.onPreviewLoaded(preview);
+ }
+ });
+ ArraySet<WidgetPreviewLoadedCallback> callbacks = new ArraySet<>();
+ callbacks.add(callback);
+ putCacheResult(
+ item,
+ previewSize,
+ new CacheResult.Loading(delegateCancellationSignal, callbacks));
+ }
+
+ return signal;
+ }
+
+ /** Clears all cached previews for {@code items}, cancelling any in-progress preview loading. */
+ public void clearPreviews(Iterable<WidgetItem> items) {
+ List<CacheResult> previousCacheResults = new ArrayList<>();
+ synchronized (mCache) {
+ for (WidgetItem item : items) {
+ Map<Size, CacheResult> previousMap = mCache.remove(toComponentKey(item));
+ if (previousMap != null) {
+ previousCacheResults.addAll(previousMap.values());
+ }
+ }
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ /** Clears all cached previews, cancelling any in-progress preview loading. */
+ public void clearAll() {
+ List<CacheResult> previousCacheResults;
+ synchronized (mCache) {
+ previousCacheResults =
+ mCache
+ .values()
+ .stream()
+ .flatMap(sizeToResult -> sizeToResult.values().stream())
+ .collect(Collectors.toList());
+ mCache.clear();
+ }
+
+ for (CacheResult previousCacheResult : previousCacheResults) {
+ if (previousCacheResult instanceof CacheResult.Loading) {
+ ((CacheResult.Loading) previousCacheResult).mCancellationSignal.cancel();
+ }
+ }
+ }
+
+ private abstract static class CacheResult {
+ static final CacheResult MISS = new CacheResult() {};
+
+ static final class Loading extends CacheResult {
+ @NonNull final CancellationSignal mCancellationSignal;
+ @NonNull final Set<WidgetPreviewLoadedCallback> mCallbacks;
+
+ Loading(@NonNull CancellationSignal cancellationSignal,
+ @NonNull Set<WidgetPreviewLoadedCallback> callbacks) {
+ mCancellationSignal = cancellationSignal;
+ mCallbacks = callbacks;
+ }
+
+ @NonNull
+ Loading withCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() + 1);
+ newCallbacks.addAll(mCallbacks);
+ newCallbacks.add(callback);
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+
+ @NonNull
+ Loading withoutCallback(@NonNull WidgetPreviewLoadedCallback callback) {
+ if (!mCallbacks.contains(callback)) return this;
+ Set<WidgetPreviewLoadedCallback> newCallbacks =
+ new ArraySet<>(mCallbacks.size() - 1);
+ for (WidgetPreviewLoadedCallback existingCallback : mCallbacks) {
+ if (!existingCallback.equals(callback)) {
+ newCallbacks.add(existingCallback);
+ }
+ }
+ return new Loading(mCancellationSignal, newCallbacks);
+ }
+ }
+
+ static final class Loaded extends CacheResult {
+ @NonNull final Bitmap mBitmap;
+
+ Loaded(@NonNull Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+ }
+ }
+
+ @NonNull
+ private static ComponentKey toComponentKey(@NonNull WidgetItem item) {
+ return new ComponentKey(item.componentName, item.user);
+ }
+}
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
similarity index 86%
rename from src/com/android/launcher3/WidgetPreviewLoader.java
rename to src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
index ff3584a..6de3e11 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
@@ -1,4 +1,19 @@
-package com.android.launcher3;
+/*
+ * Copyright (C) 2021 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.widget;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -32,8 +47,14 @@
import android.util.Pair;
import android.util.Size;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.LauncherIcons;
@@ -47,9 +68,6 @@
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SQLiteCacheHelper;
import com.android.launcher3.util.Thunk;
-import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.widget.WidgetCell;
-import com.android.launcher3.widget.WidgetManagerHelper;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
@@ -60,7 +78,8 @@
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
-public class WidgetPreviewLoader {
+/** {@link WidgetPreviewLoader} that loads preview images from a {@link CacheDb}. */
+public class DatabaseWidgetPreviewLoader implements WidgetPreviewLoader {
private static final String TAG = "WidgetPreviewLoader";
private static final boolean DEBUG = false;
@@ -80,7 +99,7 @@
private final UserCache mUserCache;
private final CacheDb mDb;
- public WidgetPreviewLoader(Context context, IconCache iconCache) {
+ public DatabaseWidgetPreviewLoader(Context context, IconCache iconCache) {
mContext = context;
mIconCache = iconCache;
mUserCache = UserCache.INSTANCE.get(context);
@@ -89,16 +108,24 @@
/**
* Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
- * called on UI thread
+ * called on UI thread.
*
* @return a request id which can be used to cancel the request.
*/
- public CancellationSignal getPreview(WidgetItem item, int previewWidth,
- int previewHeight, WidgetCell caller) {
+ @Override
+ @NonNull
+ public CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback) {
+ int previewWidth = previewSize.getWidth();
+ int previewHeight = previewSize.getHeight();
String size = previewWidth + "x" + previewHeight;
WidgetCacheKey key = new WidgetCacheKey(item.componentName, item.user, size);
- PreviewLoadTask task = new PreviewLoadTask(key, item, previewWidth, previewHeight, caller);
+ PreviewLoadTask task =
+ new PreviewLoadTask(activity, key, item, previewWidth, previewHeight, callback);
task.executeOnExecutor(Executors.THREAD_POOL_EXECUTOR);
CancellationSignal signal = new CancellationSignal();
@@ -106,6 +133,7 @@
return signal;
}
+ /** Clears the database storing previews. */
public void refresh() {
mDb.clear();
}
@@ -126,21 +154,37 @@
private static final String COLUMN_VERSION = "version";
private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
- public CacheDb(Context context) {
+ CacheDb(Context context) {
super(context, LauncherFiles.WIDGET_PREVIEWS_DB, DB_VERSION, TABLE_NAME);
}
@Override
public void onCreateTable(SQLiteDatabase database) {
- database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
- COLUMN_COMPONENT + " TEXT NOT NULL, " +
- COLUMN_USER + " INTEGER NOT NULL, " +
- COLUMN_SIZE + " TEXT NOT NULL, " +
- COLUMN_PACKAGE + " TEXT NOT NULL, " +
- COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
- COLUMN_PREVIEW_BITMAP + " BLOB, " +
- "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
+ database.execSQL("CREATE TABLE IF NOT EXISTS "
+ + TABLE_NAME
+ + " ("
+ + COLUMN_COMPONENT
+ + " TEXT NOT NULL, "
+ + COLUMN_USER
+ + " INTEGER NOT NULL, "
+ + COLUMN_SIZE
+ + " TEXT NOT NULL, "
+ + COLUMN_PACKAGE
+ + " TEXT NOT NULL, "
+ + COLUMN_LAST_UPDATED
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_VERSION
+ + " INTEGER NOT NULL DEFAULT 0, "
+ + COLUMN_PREVIEW_BITMAP
+ + " BLOB, "
+ + "PRIMARY KEY ("
+ + COLUMN_COMPONENT
+ + ", "
+ + COLUMN_USER
+ + ", "
+ + COLUMN_SIZE
+ + ") "
+ +
");");
}
}
@@ -149,7 +193,7 @@
ContentValues values = new ContentValues();
values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
values.put(CacheDb.COLUMN_USER, mUserCache.getSerialNumberForUser(key.user));
- values.put(CacheDb.COLUMN_SIZE, key.size);
+ values.put(CacheDb.COLUMN_SIZE, key.mSize);
values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
values.put(CacheDb.COLUMN_VERSION, versions[0]);
values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
@@ -157,12 +201,14 @@
mDb.insertOrReplace(values);
}
+ /** Removes the package from the preview database. */
public void removePackage(String packageName, UserHandle user) {
removePackage(packageName, user, mUserCache.getSerialNumberForUser(user));
}
- private void removePackage(String packageName, UserHandle user, long userSerial) {
- synchronized(mPackageVersions) {
+ /** Removes the package from the preview database. */
+ public void removePackage(String packageName, UserHandle user, long userSerial) {
+ synchronized (mPackageVersions) {
mPackageVersions.remove(packageName);
}
@@ -264,7 +310,7 @@
new String[]{
key.componentName.flattenToShortString(),
Long.toString(mUserCache.getSerialNumberForUser(key.user)),
- key.size
+ key.mSize
});
// If cancelled, skip getting the blob and decoding it into a bitmap
if (loadTask.isCancelled()) {
@@ -293,7 +339,7 @@
}
/**
- * Returns generatedPreview for a widget and if the preview should be saved in persistent
+ * Returns a generated preview for a widget and if the preview should be saved in persistent
* storage.
* @param launcher
* @param item
@@ -344,8 +390,10 @@
if (drawable != null) {
drawable = mutateOnMainThread(drawable);
} else {
- Log.w(TAG, "Can't load widget preview drawable 0x" +
- Integer.toHexString(info.previewImage) + " for provider: " + info.provider);
+ Log.w(TAG, "Can't load widget preview drawable 0x"
+ + Integer.toHexString(info.previewImage)
+ + " for provider: "
+ + info.provider);
}
}
@@ -379,8 +427,8 @@
scale = maxPreviewWidth / (float) (previewWidth);
}
if (scale != 1f) {
- previewWidth = Math.max((int)(scale * previewWidth), 1);
- previewHeight = Math.max((int)(scale * previewHeight), 1);
+ previewWidth = Math.max((int) (scale * previewWidth), 1);
+ previewHeight = Math.max((int) (scale * previewHeight), 1);
}
final Canvas c = new Canvas();
@@ -554,13 +602,13 @@
}
}
- public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
+ private class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap>
implements CancellationSignal.OnCancelListener {
@Thunk final WidgetCacheKey mKey;
private final WidgetItem mInfo;
private final int mPreviewHeight;
private final int mPreviewWidth;
- private final WidgetCell mCaller;
+ private final WidgetPreviewLoadedCallback mCallback;
private final BaseActivity mActivity;
@Thunk long[] mVersions;
@Thunk Bitmap mBitmapToRecycle;
@@ -568,14 +616,14 @@
@Nullable private Bitmap mUnusedPreviewBitmap;
private boolean mSaveToDB = false;
- PreviewLoadTask(WidgetCacheKey key, WidgetItem info, int previewWidth,
- int previewHeight, WidgetCell caller) {
+ PreviewLoadTask(BaseActivity activity, WidgetCacheKey key, WidgetItem info,
+ int previewWidth, int previewHeight, WidgetPreviewLoadedCallback callback) {
+ mActivity = activity;
mKey = key;
mInfo = info;
mPreviewHeight = previewHeight;
mPreviewWidth = previewWidth;
- mCaller = caller;
- mActivity = BaseActivity.fromContext(mCaller.getContext());
+ mCallback = callback;
if (DEBUG) {
Log.d(TAG, String.format("%s, %s, %d, %d",
mKey, mInfo, mPreviewHeight, mPreviewWidth));
@@ -593,9 +641,9 @@
synchronized (mUnusedBitmaps) {
// Check if we can re-use a bitmap
for (Bitmap candidate : mUnusedBitmaps) {
- if (candidate != null && candidate.isMutable() &&
- candidate.getWidth() == mPreviewWidth &&
- candidate.getHeight() == mPreviewHeight) {
+ if (candidate != null && candidate.isMutable()
+ && candidate.getWidth() == mPreviewWidth
+ && candidate.getHeight() == mPreviewHeight) {
unusedBitmap = candidate;
mUnusedBitmaps.remove(unusedBitmap);
break;
@@ -638,7 +686,7 @@
@Override
protected void onPostExecute(final Bitmap preview) {
- mCaller.applyPreview(preview);
+ mCallback.onPreviewLoaded(preview);
// Write the generated preview to the DB in the worker thread
if (mVersions != null) {
@@ -716,21 +764,21 @@
private static final class WidgetCacheKey extends ComponentKey {
- @Thunk final String size;
+ @Thunk final String mSize;
- public WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
+ WidgetCacheKey(ComponentName componentName, UserHandle user, String size) {
super(componentName, user);
- this.size = size;
+ this.mSize = size;
}
@Override
public int hashCode() {
- return super.hashCode() ^ size.hashCode();
+ return super.hashCode() ^ mSize.hashCode();
}
@Override
public boolean equals(Object o) {
- return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
+ return super.equals(o) && ((WidgetCacheKey) o).mSize.equals(mSize);
}
}
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 50ab422..fa50dfb 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -16,26 +16,20 @@
package com.android.launcher3.widget;
-import static com.android.launcher3.Utilities.getBoundsForViewInDragLayer;
-import static com.android.launcher3.Utilities.setRect;
-
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
-import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Advanceable;
@@ -43,7 +37,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
@@ -51,9 +44,9 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
@@ -63,7 +56,7 @@
/**
* {@inheritDoc}
*/
-public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
+public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
implements TouchCompleteListener, View.OnLongClickListener,
LocalColorExtractor.Listener {
@@ -78,8 +71,6 @@
// Maximum duration for which updates can be deferred.
private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
- protected final LayoutInflater mInflater;
-
private final CheckLongPressHelper mLongPressHelper;
protected final Launcher mLauncher;
private final Workspace mWorkspace;
@@ -102,21 +93,9 @@
private final Rect mCurrentWidgetSize = new Rect();
private final Rect mWidgetSizeAtDrag = new Rect();
- private final float[] mTmpFloatArray = new float[4];
private final RectF mTempRectF = new RectF();
- private final Rect mEnforcedRectangle = new Rect();
- private final float mEnforcedCornerRadius;
- private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
- outline.setEmpty();
- } else {
- outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
- }
- }
- };
private final Object mUpdateLock = new Object();
+ private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
private long mDeferUpdatesUntilMillis = 0;
private RemoteViews mMostRecentRemoteViews;
@@ -125,18 +104,16 @@
mLauncher = Launcher.getLauncher(context);
mWorkspace = mLauncher.getWorkspace();
mLongPressHelper = new CheckLongPressHelper(this, this);
- mInflater = LayoutInflater.from(context);
setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
setBackgroundResource(R.drawable.widget_internal_focus_bg);
- setExecutor(Executors.THREAD_POOL_EXECUTOR);
if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
setOnLightBackground(true);
}
mColorExtractor = LocalColorExtractor.newInstance(getContext());
mColorExtractor.setListener(this);
- mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
}
@Override
@@ -167,11 +144,6 @@
}
@Override
- protected View getErrorView() {
- return mInflater.inflate(R.layout.appwidget_error, this, false);
- }
-
- @Override
public void updateAppWidget(RemoteViews remoteViews) {
synchronized (mUpdateLock) {
mMostRecentRemoteViews = remoteViews;
@@ -305,40 +277,17 @@
}
}
- public void switchToErrorView() {
- // Update the widget with 0 Layout id, to reset the view to error view.
- updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
- }
-
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- try {
- super.onLayout(changed, left, top, right, bottom);
- } catch (final RuntimeException e) {
- post(new Runnable() {
- @Override
- public void run() {
- switchToErrorView();
- }
- });
- }
+ super.onLayout(changed, left, top, right, bottom);
mIsScrollable = checkScrollableRecursively(this);
if (!mIsInDragMode && getTag() instanceof LauncherAppWidgetInfo) {
- mCurrentWidgetSize.left = left;
- mCurrentWidgetSize.right = right;
- mCurrentWidgetSize.top = top;
- mCurrentWidgetSize.bottom = bottom;
-
LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
- getBoundsForViewInDragLayer(mLauncher.getDragLayer(), (View) getParent(),
- mCurrentWidgetSize, true, mTmpFloatArray, mTempRectF);
- setRect(mTempRectF, mCurrentWidgetSize);
+ mDragLayerRelativeCoordinateHelper.viewToRect(this, mCurrentWidgetSize);
updateColorExtraction(mCurrentWidgetSize,
mWorkspace.getPageIndexForScreenId(info.screenId));
}
-
- enforceRoundedCorners();
}
/** Starts the drag mode. */
@@ -358,7 +307,6 @@
mIsInDragMode = false;
mDragListener = null;
mWidgetSizeAtDrag.setEmpty();
- requestLayout();
}
/**
@@ -510,40 +458,4 @@
}
return false;
}
-
- @UiThread
- private void resetRoundedCorners() {
- setOutlineProvider(ViewOutlineProvider.BACKGROUND);
- setClipToOutline(false);
- }
-
- @UiThread
- private void enforceRoundedCorners() {
- if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
- resetRoundedCorners();
- return;
- }
- View background = RoundedCornerEnforcement.findBackground(this);
- if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
- resetRoundedCorners();
- return;
- }
- RoundedCornerEnforcement.computeRoundedRectangle(this,
- background,
- mEnforcedRectangle);
- setOutlineProvider(mCornerRadiusEnforcementOutline);
- setClipToOutline(true);
- }
-
- /** Returns the corner radius currently enforced, in pixels. */
- public float getEnforcedCornerRadius() {
- return mEnforcedCornerRadius;
- }
-
- /** Returns true if the corner radius are enforced for this App Widget. */
- public boolean hasEnforcedCornerRadius() {
- return getClipToOutline();
- }
-
}
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 6163b51..d12fe74 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -26,7 +26,6 @@
import android.view.ViewDebug;
import android.view.ViewGroup;
-import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Reorderable;
import com.android.launcher3.dragndrop.DraggableView;
@@ -59,7 +58,7 @@
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mChildrenFocused;
- protected final BaseActivity mActivity;
+ protected final ActivityContext mActivity;
public NavigableAppWidgetHostView(Context context) {
super(context);
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 3308eec..47a8914 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -97,9 +97,9 @@
@Override
public void updateAppWidget(RemoteViews remoteViews) {
- super.updateAppWidget(remoteViews);
WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(getContext());
if (widgetManagerHelper.isAppWidgetRestored(mInfo.appWidgetId)) {
+ super.updateAppWidget(remoteViews);
reInflate();
}
}
@@ -149,7 +149,7 @@
// The view displays three modes,
// 1) App icon in the center
// 2) Preload icon in the center
- // 3) Setup icon in the center and app icon in the top right corner.
+ // 3) App icon in the center with a setup icon on the top left corner.
if (mDisabledForSafeMode) {
FastBitmapDrawable disabledIcon = info.newIcon(getContext());
disabledIcon.setIsDisabled(true);
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index b1ccfd9..91529be 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -19,7 +19,6 @@
import static com.android.launcher3.Utilities.ATLEAST_S;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -44,7 +43,6 @@
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.model.WidgetItem;
@@ -222,21 +220,18 @@
return;
}
- if (ATLEAST_S
- && mRemoteViewsPreview == null
- && item.widgetInfo != null
- && item.widgetInfo.previewLayout != Resources.ID_NULL) {
- mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext());
- LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
- LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
- item.widgetInfo.clone());
- // A hack to force the initial layout to be the preview layout since there is no API for
- // rendering a preview layout for work profile apps yet. For non-work profile layout, a
- // proper solution is to use RemoteViews(PackageName, LayoutId).
- launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
- setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
- launcherAppWidgetProviderInfo, /* remoteViews= */ null);
- }
+ if (!item.hasPreviewLayout()) return;
+
+ mAppWidgetHostViewPreview = new LauncherAppWidgetHostView(getContext());
+ LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+ LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
+ item.widgetInfo.clone());
+ // A hack to force the initial layout to be the preview layout since there is no API for
+ // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+ // proper solution is to use RemoteViews(PackageName, LayoutId).
+ launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
+ setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
+ launcherAppWidgetProviderInfo, /* remoteViews= */ null);
}
private void setAppWidgetHostViewPreview(
@@ -344,22 +339,25 @@
if (mActiveRequest != null) {
return;
}
- mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight,
- this);
+ mActiveRequest = mWidgetPreviewLoader.loadPreview(
+ BaseActivity.fromContext(getContext()), mItem,
+ new Size(mPreviewWidth, mPreviewHeight),
+ this::applyPreview);
}
/** Sets the widget preview image size in number of cells. */
- public void setPreviewSize(int spanX, int spanY) {
- setPreviewSize(spanX, spanY, 1f);
+ public Size setPreviewSize(int spanX, int spanY) {
+ return setPreviewSize(spanX, spanY, 1f);
}
/** Sets the widget preview image size, in number of cells, and preview scale. */
- public void setPreviewSize(int spanX, int spanY, float previewScale) {
+ public Size setPreviewSize(int spanX, int spanY, float previewScale) {
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, spanX, spanY);
mPreviewWidth = widgetSize.getWidth();
mPreviewHeight = widgetSize.getHeight();
mPreviewScale = previewScale;
+ return widgetSize;
}
@Override
diff --git a/src/com/android/launcher3/widget/WidgetPreviewLoader.java b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
new file mode 100644
index 0000000..ff5c82f
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetPreviewLoader.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import android.graphics.Bitmap;
+import android.os.CancellationSignal;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.model.WidgetItem;
+
+/** Asynchronous loader of preview bitmaps for {@link WidgetItem}s. */
+public interface WidgetPreviewLoader {
+ /**
+ * Loads a widget preview and calls back to {@code callback} when complete.
+ *
+ * @return a {@link CancellationSignal} which can be used to cancel the request.
+ */
+ @NonNull
+ @UiThread
+ CancellationSignal loadPreview(
+ @NonNull BaseActivity activity,
+ @NonNull WidgetItem item,
+ @NonNull Size previewSize,
+ @NonNull WidgetPreviewLoadedCallback callback);
+
+ /** Callback class for requests to {@link WidgetPreviewLoader}. */
+ interface WidgetPreviewLoadedCallback {
+ void onPreviewLoaded(@NonNull Bitmap preview);
+ }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
index 73bae6f..abc79ff 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -59,6 +59,19 @@
@Rank
public abstract int getRank();
+ /**
+ * Marker interface for subclasses that are headers for widget list items.
+ *
+ * @param <T> The type of this class.
+ */
+ public interface Header<T extends WidgetsListBaseEntry & Header<T>> {
+ /** Returns whether the widget list is currently expanded. */
+ boolean isWidgetListShown();
+
+ /** Returns a copy of the item with the widget list shown. */
+ T withWidgetListShown();
+ }
+
@Retention(SOURCE)
@IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT})
public @interface Rank {
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
index 1fdc399..5b3ea94 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -21,41 +21,33 @@
import java.util.List;
/** An information holder for an app which has widgets or/and shortcuts. */
-public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
+public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListHeaderEntry> {
public final int widgetsCount;
public final int shortcutsCount;
- private boolean mIsWidgetListShown = false;
- private boolean mHasEntryUpdated = false;
+ private final boolean mIsWidgetListShown;
public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items) {
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
+ }
+
+ private WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
super(pkgItem, titleSectionName, items);
widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
shortcutsCount = Math.max(0, items.size() - widgetsCount);
- }
-
- /** Sets if the widgets list associated with this header is shown. */
- public void setIsWidgetListShown(boolean isWidgetListShown) {
- if (mIsWidgetListShown != isWidgetListShown) {
- this.mIsWidgetListShown = isWidgetListShown;
- mHasEntryUpdated = true;
- } else {
- mHasEntryUpdated = false;
- }
+ mIsWidgetListShown = isWidgetListShown;
}
/** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
public boolean isWidgetListShown() {
return mIsWidgetListShown;
}
- /** Returns {@code true} if this entry has been updated due to user interactions. */
- public boolean hasEntryUpdated() {
- return mHasEntryUpdated;
- }
-
@Override
public String toString() {
return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
@@ -72,6 +64,18 @@
if (!(obj instanceof WidgetsListHeaderEntry)) return false;
WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj;
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
- && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown == otherEntry.mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
}
}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
index 2aec3f8..c0f89bc 100644
--- a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
+++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java
@@ -21,36 +21,28 @@
import java.util.List;
/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */
-public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry {
+public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry
+ implements WidgetsListBaseEntry.Header<WidgetsListSearchHeaderEntry> {
- private boolean mIsWidgetListShown = false;
- private boolean mHasEntryUpdated = false;
+ private final boolean mIsWidgetListShown;
public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
List<WidgetItem> items) {
- super(pkgItem, titleSectionName, items);
+ this(pkgItem, titleSectionName, items, /* isWidgetListShown= */ false);
}
- /** Sets if the widgets list associated with this header is shown. */
- public void setIsWidgetListShown(boolean isWidgetListShown) {
- if (mIsWidgetListShown != isWidgetListShown) {
- this.mIsWidgetListShown = isWidgetListShown;
- mHasEntryUpdated = true;
- } else {
- mHasEntryUpdated = false;
- }
+ private WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+ List<WidgetItem> items, boolean isWidgetListShown) {
+ super(pkgItem, titleSectionName, items);
+ mIsWidgetListShown = isWidgetListShown;
}
/** Returns {@code true} if the widgets list associated with this header is shown. */
+ @Override
public boolean isWidgetListShown() {
return mIsWidgetListShown;
}
- /** Returns {@code true} if this entry has been updated due to user interactions. */
- public boolean hasEntryUpdated() {
- return mHasEntryUpdated;
- }
-
@Override
public String toString() {
return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size();
@@ -67,6 +59,18 @@
if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false;
WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj;
return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem)
- && mTitleSectionName.equals(otherEntry.mTitleSectionName);
+ && mTitleSectionName.equals(otherEntry.mTitleSectionName)
+ && mIsWidgetListShown;
+ }
+
+ /** Returns a copy of this {@link WidgetsListSearchHeaderEntry} with the widget list shown. */
+ @Override
+ public WidgetsListSearchHeaderEntry withWidgetListShown() {
+ if (mIsWidgetListShown) return this;
+ return new WidgetsListSearchHeaderEntry(
+ mPkgItem,
+ mTitleSectionName,
+ mWidgets,
+ /* isWidgetListShown= */ true);
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
index 42896ba..dfe447a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -177,7 +177,7 @@
*/
private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
- return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow);
+ return !curRow.equals(newRow);
}
if (newRow instanceof WidgetsListSearchHeaderEntry
&& curRow instanceof WidgetsListSearchHeaderEntry) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 6863c60..5d9adf0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.os.Process;
import android.util.Log;
+import android.util.Size;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
@@ -35,19 +36,25 @@
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.LabelComparator;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
+import com.android.launcher3.widget.DatabaseWidgetPreviewLoader;
import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetPreviewLoader.WidgetPreviewLoadedCallback;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
+import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
import java.util.Arrays;
@@ -79,7 +86,9 @@
private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
+ private final Context mContext;
private final Launcher mLauncher;
+ private final CachingWidgetPreviewLoader mCachingPreviewLoader;
private final WidgetsDiffReporter mDiffReporter;
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
@@ -97,16 +106,23 @@
.equals(mWidgetsContentVisiblePackageUserKey);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
@Nullable private RecyclerView mRecyclerView;
+ @Nullable private PackageUserKey mPendingClickHeader;
+ private int mShortcutPreviewPadding;
+
+ private final WidgetPreviewLoadedCallback mPreviewLoadedCallback =
+ ignored -> updateVisibleEntries();
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
- WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+ DatabaseWidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
+ mContext = context;
mLauncher = Launcher.getLauncher(context);
+ mCachingPreviewLoader = new CachingWidgetPreviewLoader(widgetPreviewLoader);
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);
- mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
+ mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(
layoutInflater, iconClickListener, iconLongClickListener,
- widgetPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
+ mCachingPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
mViewHolderBinders.put(
VIEW_TYPE_WIDGETS_HEADER,
@@ -122,6 +138,9 @@
/* onHeaderClickListener= */ this,
listDrawableFactory,
/* listAdapter= */ this));
+ mShortcutPreviewPadding =
+ 2 * context.getResources()
+ .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
}
@Override
@@ -177,6 +196,7 @@
/** Updates the widget list based on {@code tempEntries}. */
public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
+ mCachingPreviewLoader.clearAll();
mAllEntries = tempEntries.stream().sorted(mRowComparator)
.collect(Collectors.toList());
if (shouldClearVisibleEntries()) {
@@ -189,36 +209,110 @@
public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
// Forget the expanded package every time widget list is refreshed in search mode.
mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
setWidgets(searchResults);
}
private void updateVisibleEntries() {
- mAllEntries.forEach(entry -> {
- if (entry instanceof WidgetsListHeaderEntry) {
- ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
- isHeaderForVisibleContent(entry));
- } else if (entry instanceof WidgetsListSearchHeaderEntry) {
- ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown(
- isHeaderForVisibleContent(entry));
- }
- });
+ // If not all previews are ready, then defer this update and try again after the preview
+ // loads.
+ if (!ensureAllPreviewsReady()) return;
+
+ // Get the current top of the header with the matching key before adjusting the visible
+ // entries.
+ OptionalInt previousPositionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ OptionalInt topForPackageUserKey =
+ getOffsetForPosition(previousPositionForPackageUserKey);
+
List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
.filter(entry -> (mFilter == null || mFilter.test(entry))
&& mHeaderAndSelectedContentFilter.test(entry))
+ .map(entry -> {
+ // Adjust the original entries to expand headers for the selected content.
+ if (entry instanceof WidgetsListBaseEntry.Header<?>
+ && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ return ((WidgetsListBaseEntry.Header<?>) entry).withWidgetListShown();
+ }
+ return entry;
+ })
.collect(Collectors.toList());
+
mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
+
+ if (mPendingClickHeader != null) {
+ // Get the position for the clicked header after adjusting the visible entries. The
+ // position may have changed if another header had previously been expanded.
+ OptionalInt positionForPackageUserKey =
+ getPositionForPackageUserKey(mPendingClickHeader);
+ scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
+ mPendingClickHeader = null;
+ }
}
- /** Returns whether {@code entry} matches {@link #mWidgetsContentVisiblePackageUserKey}. */
- private boolean isHeaderForVisibleContent(WidgetsListBaseEntry entry) {
- return isHeaderForPackageUserKey(entry, mWidgetsContentVisiblePackageUserKey);
+ /**
+ * Checks that all preview images are loaded and starts loading for those that aren't ready.
+ *
+ * @return true if all previews are ready and the data can be updated, false otherwise.
+ */
+ private boolean ensureAllPreviewsReady() {
+ boolean allReady = true;
+ BaseActivity activity = BaseActivity.fromContext(mContext);
+ for (WidgetsListBaseEntry entry : mAllEntries) {
+ if (!(entry instanceof WidgetsListContentEntry)) continue;
+
+ WidgetsListContentEntry contentEntry = (WidgetsListContentEntry) entry;
+ if (!matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
+ // If the entry isn't visible, clear any loaded previews.
+ mCachingPreviewLoader.clearPreviews(contentEntry.mWidgets);
+ continue;
+ }
+
+ for (int i = 0; i < entry.mWidgets.size(); i++) {
+ WidgetItem widgetItem = entry.mWidgets.get(i);
+ DeviceProfile deviceProfile = activity.getDeviceProfile();
+ Size widgetSize =
+ WidgetSizes.getWidgetSizePx(
+ deviceProfile,
+ widgetItem.spanX,
+ widgetItem.spanY);
+ if (widgetItem.isShortcut()) {
+ widgetSize =
+ new Size(
+ widgetSize.getWidth() + mShortcutPreviewPadding,
+ widgetSize.getHeight() + mShortcutPreviewPadding);
+ }
+
+ if (widgetItem.hasPreviewLayout()
+ || mCachingPreviewLoader.isPreviewLoaded(widgetItem, widgetSize)) {
+ // The widget is ready if it can be rendered with a preview layout or if its
+ // preview bitmap is in the cache.
+ continue;
+ }
+
+ // If we've reached this point, we should load the preview for the widget.
+ allReady = false;
+ mCachingPreviewLoader.loadPreview(
+ activity,
+ widgetItem,
+ widgetSize,
+ mPreviewLoadedCallback);
+ }
+ }
+ return allReady;
}
/** Returns whether {@code entry} matches {@code key}. */
- private boolean isHeaderForPackageUserKey(WidgetsListBaseEntry entry, PackageUserKey key) {
- return (entry instanceof WidgetsListHeaderEntry
- || entry instanceof WidgetsListSearchHeaderEntry)
- && new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user).equals(key);
+ private static boolean isHeaderForPackageUserKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key);
+ }
+
+ private static boolean matchesKey(
+ @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
+ if (key == null) return false;
+ return entry.mPkgItem.packageName.equals(key.mPackageName)
+ && entry.mPkgItem.user.equals(key.mUser);
}
/**
@@ -227,6 +321,7 @@
public void resetExpandedHeader() {
if (mWidgetsContentVisiblePackageUserKey != null) {
mWidgetsContentVisiblePackageUserKey = null;
+ cancelLoadingPreviews();
updateVisibleEntries();
}
}
@@ -285,6 +380,8 @@
// Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
+ cancelLoadingPreviews();
+
if (showWidgets) {
mWidgetsContentVisiblePackageUserKey = packageUserKey;
mLauncher.getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED);
@@ -292,17 +389,15 @@
mWidgetsContentVisiblePackageUserKey = null;
}
- // Get the current top of the header with the matching key before adjusting the visible
- // entries.
- OptionalInt topForPackageUserKey =
- getOffsetForPosition(getPositionForPackageUserKey(packageUserKey));
+ // Store the header that was clicked so that its position will be maintained the next time
+ // we update the entries.
+ mPendingClickHeader = packageUserKey;
updateVisibleEntries();
+ }
- // Get the position for the clicked header after adjusting the visible entries. The
- // position may have changed if another header had previously been expanded.
- OptionalInt positionForPackageUserKey = getPositionForPackageUserKey(packageUserKey);
- scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
+ private void cancelLoadingPreviews() {
+ mCachingPreviewLoader.clearAll();
}
/** Returns the position of the currently expanded header, or empty if it's not present. */
@@ -315,7 +410,8 @@
* Returns the position of {@code key} in {@link #mVisibleEntries}, or empty if it's not
* present.
*/
- private OptionalInt getPositionForPackageUserKey(PackageUserKey key) {
+ @NonNull
+ private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) {
return IntStream.range(0, mVisibleEntries.size())
.filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key))
.findFirst();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 7e8c55b..7b52663 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -18,8 +18,9 @@
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
-import android.content.Context;
+import android.graphics.Bitmap;
import android.util.Log;
+import android.util.Size;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,9 +31,9 @@
import android.widget.TableRow;
import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.CachingWidgetPreviewLoader;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.util.WidgetsTableUtils;
@@ -52,17 +53,16 @@
private final LayoutInflater mLayoutInflater;
private final OnClickListener mIconClickListener;
private final OnLongClickListener mIconLongClickListener;
- private final WidgetPreviewLoader mWidgetPreviewLoader;
private final WidgetsListDrawableFactory mListDrawableFactory;
+ private final CachingWidgetPreviewLoader mWidgetPreviewLoader;
private final WidgetsListAdapter mWidgetsListAdapter;
private boolean mApplyBitmapDeferred = false;
public WidgetsListTableViewHolderBinder(
- Context context,
LayoutInflater layoutInflater,
OnClickListener iconClickListener,
OnLongClickListener iconLongClickListener,
- WidgetPreviewLoader widgetPreviewLoader,
+ CachingWidgetPreviewLoader widgetPreviewLoader,
WidgetsListDrawableFactory listDrawableFactory,
WidgetsListAdapter listAdapter) {
mLayoutInflater = layoutInflater;
@@ -75,7 +75,7 @@
/**
* Defers applying bitmap on all the {@link WidgetCell} at
- * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if
+ * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry, int)} if
* {@code applyBitmapDeferred} is {@code true}.
*/
public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
@@ -124,10 +124,15 @@
WidgetCell widget = (WidgetCell) row.getChildAt(j);
widget.clear();
WidgetItem widgetItem = widgetItemsPerRow.get(j);
- widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+ Size previewSize = widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
- widget.ensurePreview();
+ Bitmap preview = mWidgetPreviewLoader.getPreview(widgetItem, previewSize);
+ if (preview == null) {
+ widget.ensurePreview();
+ } else {
+ widget.applyPreview(preview);
+ }
widget.setVisibility(View.VISIBLE);
}
}
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index b35c75b..df5d465 100644
--- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -127,7 +127,7 @@
@Override
public void setActiveMarker(int activePage) {
updateTabTextColor(activePage);
- updateIndicatorPosition(activePage);
+ updateIndicatorPosition(mIsRtl ? 1 - activePage : activePage);
if (mOnActivePageChangedListener != null && mLastActivePage != activePage) {
mOnActivePageChangedListener.onActivePageChanged(activePage);
}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 51c5b12..a66b031 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -15,6 +15,7 @@
import android.util.Log;
import androidx.annotation.Nullable;
+import androidx.collection.ArrayMap;
import com.android.launcher3.AppFilter;
import com.android.launcher3.InvariantDeviceProfile;
@@ -37,6 +38,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -158,34 +160,29 @@
Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
}
- // Temporary list for {@link PackageItemInfos} to avoid having to go through
+ // Temporary cache for {@link PackageItemInfos} to avoid having to go through
// {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
- HashMap<WidgetPackageOrCategoryKey, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
+ PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache();
- // Clear the lists only if this is an update on all widgets and shortcuts. If packageUser
- // isn't null, only updates the shortcuts and widgets for the app represented in
- // packageUser.
if (packageUser == null) {
+ // Clear the list if this is an update on all widgets and shortcuts.
mWidgetsList.clear();
+ } else {
+ // Otherwise, only clear the widgets and shortcuts for the changed package.
+ mWidgetsList.remove(
+ packageItemInfoCache.getOrCreate(new WidgetPackageOrCategoryKey(packageUser)));
}
+
// add and update.
mWidgetsList.putAll(rawWidgetsShortcuts.stream()
.filter(new WidgetValidityCheck(app))
- .collect(Collectors.groupingBy(item -> {
- WidgetPackageOrCategoryKey packageUserKey = getWidgetPackageOrCategoryKey(item);
- PackageItemInfo pInfo = tmpPackageItemInfos.get(packageUserKey);
- if (pInfo == null) {
- pInfo = new PackageItemInfo(item.componentName.getPackageName(),
- packageUserKey.mCategory);
- pInfo.user = item.user;
- tmpPackageItemInfos.put(packageUserKey, pInfo);
- }
- return pInfo;
- })));
+ .collect(Collectors.groupingBy(item ->
+ packageItemInfoCache.getOrCreate(getWidgetPackageOrCategoryKey(item))
+ )));
// Update each package entry
IconCache iconCache = app.getIconCache();
- for (PackageItemInfo p : tmpPackageItemInfos.values()) {
+ for (PackageItemInfo p : packageItemInfoCache.values()) {
iconCache.getTitleAndIconForApp(p, true /* userLowResIcon */);
}
}
@@ -289,6 +286,10 @@
public final UserHandle mUser;
private final int mHashCode;
+ WidgetPackageOrCategoryKey(PackageUserKey key) {
+ this(key.mPackageName, key.mUser);
+ }
+
WidgetPackageOrCategoryKey(String packageName, UserHandle user) {
this(packageName, PackageItemInfo.NO_CATEGORY, user);
}
@@ -310,4 +311,22 @@
return mHashCode;
}
}
+
+ private static final class PackageItemInfoCache {
+ private final Map<WidgetPackageOrCategoryKey, PackageItemInfo> mMap = new ArrayMap<>();
+
+ PackageItemInfo getOrCreate(WidgetPackageOrCategoryKey key) {
+ PackageItemInfo pInfo = mMap.get(key);
+ if (pInfo == null) {
+ pInfo = new PackageItemInfo(key.mPackage, key.mCategory);
+ pInfo.user = key.mUser;
+ mMap.put(key, pInfo);
+ }
+ return pInfo;
+ }
+
+ Collection<PackageItemInfo> values() {
+ return mMap.values();
+ }
+ }
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f5c1efa..96e8222 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -39,6 +39,7 @@
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
+import android.os.DeadObjectException;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -269,6 +270,9 @@
try (ContentProviderClient client = getContext().getContentResolver()
.acquireContentProviderClient(mTestProviderUri)) {
return client.call(request, null, null);
+ } catch (DeadObjectException e) {
+ fail("Launcher crashed");
+ return null;
} catch (RemoteException e) {
throw new RuntimeException(e);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 51e331d..99d9889 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -185,8 +185,12 @@
targetAppSelector);
if (headerTitle != null) {
// If we find the header and it has not been expanded, let's click it to see the
- // widgets list.
- if (!hasHeaderExpanded) {
+ // widgets list. Note that we wait until the header is out of the gesture region at
+ // the bottom of the screen, because tapping there in Launcher3 causes NexusLauncher
+ // to briefly appear to handle the gesture, which can break our test.
+ boolean isHeaderOutOfGestureRegion = headerTitle.getVisibleCenter().y
+ < mLauncher.getBottomGestureStartOnScreen();
+ if (!hasHeaderExpanded && isHeaderOutOfGestureRegion) {
log("Header has not been expanded. Click to expand.");
hasHeaderExpanded = true;
mLauncher.clickLauncherObject(headerTitle);