diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index 80cdbe98cb..64321192c6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -454,6 +454,10 @@ import java.util.StringJoiner; } private void updateIconAlphaForHome(float alpha) { + if (mControllers.taskbarActivityContext.isDestroyed()) { + Log.e("b/260135164", "updateIconAlphaForHome is called after Taskbar is destroyed", + new Exception()); + } mIconAlphaForHome.setValue(alpha); boolean hotseatVisible = alpha == 0 || (!mControllers.uiController.isHotseatIconOnTopWhenAligned() diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java index 80f030fb69..a6b2a8abd0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java @@ -113,7 +113,8 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable return; } reset(); - if (mControllers.taskbarStashController.isTaskbarVisibleAndNotStashing()) { + if (mControllers.taskbarStashController.isInApp() + && mControllers.taskbarStashController.isTaskbarVisibleAndNotStashing()) { mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu(); } })); diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 46d7409512..8c4759ec2d 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -1624,10 +1624,13 @@ public abstract class RecentsView diff --git a/res/layout/widgets_full_sheet_large_screen.xml b/res/layout/widgets_full_sheet_large_screen.xml index 3dbe6f5bf8..1c0037d698 100644 --- a/res/layout/widgets_full_sheet_large_screen.xml +++ b/res/layout/widgets_full_sheet_large_screen.xml @@ -24,7 +24,6 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@drawable/bg_widgets_full_sheet" android:focusable="true" android:importantForAccessibility="no"> diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml index 2819b990a9..b02e3e3c3f 100644 --- a/res/layout/widgets_full_sheet_paged_view.xml +++ b/res/layout/widgets_full_sheet_paged_view.xml @@ -47,6 +47,7 @@ android:layout_height="wrap_content" android:layout_below="@id/collapse_handle" android:paddingBottom="0dp" + android:clipToOutline="true" android:orientation="vertical"> 0) { - leftSpacing = inlineNavButtonsEndSpacing; - rightSpacing = availableWidthPx - hotseatWidth - leftSpacing + hotseatBorderSpace; + startSpacing = inlineNavButtonsEndSpacing; + endSpacing = availableWidthPx - hotseatWidth - startSpacing + hotseatBorderSpace; + } else { + startSpacing = (availableWidthPx - hotseatWidth) / 2; + endSpacing = startSpacing; } + startSpacing += getAdditionalQsbSpace(); - hotseatBarPadding.set(leftSpacing, hotseatBarTopPadding, rightSpacing, - hotseatBarBottomPadding); - + hotseatBarPadding.top = hotseatBarTopPadding; + hotseatBarPadding.bottom = hotseatBarBottomPadding; boolean isRtl = Utilities.isRtl(context.getResources()); if (isRtl) { - hotseatBarPadding.right += getAdditionalQsbSpace(); + hotseatBarPadding.left = endSpacing; + hotseatBarPadding.right = startSpacing; } else { - hotseatBarPadding.left += getAdditionalQsbSpace(); + hotseatBarPadding.left = startSpacing; + hotseatBarPadding.right = endSpacing; } + } else if (isScalableGrid) { int sideSpacing = (availableWidthPx - hotseatQsbWidth) / 2; hotseatBarPadding.set(sideSpacing, diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 7641728e70..14e3fa6fa4 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -16,10 +16,10 @@ package com.android.launcher3.allapps; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; -import static com.android.launcher3.allapps.AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_COUNT; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB; +import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -526,7 +526,7 @@ public class ActivityAllAppsContainerView public void getOutline(View view, Outline outline) { @Px final int bottomOffsetPx = (int) (ActivityAllAppsContainerView.this.getMeasuredHeight() - * SWIPE_ALL_APPS_TO_HOME_MIN_SCALE); + * PREDICTIVE_BACK_MIN_SCALE); outline.setRect( 0, 0, diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index 866932ab16..df383bf9f6 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -21,7 +21,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; -import androidx.annotation.Px; import androidx.core.view.accessibility.AccessibilityEventCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityRecordCompat; @@ -145,19 +144,6 @@ public class AllAppsGridAdapter extends cic.isSelected())); } - /** - * We need to extend all apps' RecyclerView's bottom by 5% of view height to ensure extra - * roll(s) of app icons is rendered at the bottom, so that they can fill the bottom gap - * created during predictive back's scale animation from all apps to home. - */ - @Override - protected void calculateExtraLayoutSpace(RecyclerView.State state, int[] extraLayoutSpace) { - super.calculateExtraLayoutSpace(state, extraLayoutSpace); - @Px int extraSpacePx = (int) (getHeight() - * (1 - AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) / 2); - extraLayoutSpace[1] = Math.max(extraLayoutSpace[1], extraSpacePx); - } - /** * Returns the number of rows before {@param adapterPosition}, including this position * which should not be counted towards the collection info. diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index b6187244f2..92c017ce88 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -61,6 +61,7 @@ import com.android.launcher3.touch.AllAppsSwipeController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.ScrollableLayoutManager; import com.android.launcher3.util.Themes; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.views.ScrimView; @@ -79,8 +80,7 @@ public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener { // This constant should match the second derivative of the animator interpolator. public static final float INTERP_COEFF = 1.7f; - public static final float SWIPE_ALL_APPS_TO_HOME_MIN_SCALE = 0.9f; - private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200; + public static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200; private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f; private static final float SWIPE_DRAG_COMMIT_THRESHOLD = @@ -280,8 +280,9 @@ public class AllAppsTransitionController float deceleratedProgress = Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(backProgress); - float scaleProgress = SWIPE_ALL_APPS_TO_HOME_MIN_SCALE - + (1 - SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) * (1 - deceleratedProgress); + float scaleProgress = ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE + + (1 - ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE) + * (1 - deceleratedProgress); mAllAppScale.updateValue(scaleProgress); } diff --git a/src/com/android/launcher3/util/ScrollableLayoutManager.java b/src/com/android/launcher3/util/ScrollableLayoutManager.java index 9bc4ddce38..cb6ecaa664 100644 --- a/src/com/android/launcher3/util/ScrollableLayoutManager.java +++ b/src/com/android/launcher3/util/ScrollableLayoutManager.java @@ -20,6 +20,7 @@ import android.util.SparseIntArray; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Px; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; @@ -31,6 +32,10 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder; */ public class ScrollableLayoutManager extends GridLayoutManager { + public static final float PREDICTIVE_BACK_MIN_SCALE = 0.9f; + private static final float EXTRA_BOTTOM_SPACE_BY_HEIGHT_PERCENT = + (1 - PREDICTIVE_BACK_MIN_SCALE) / 2; + // keyed on item type protected final SparseIntArray mCachedSizes = new SparseIntArray(); @@ -111,6 +116,13 @@ public class ScrollableLayoutManager extends GridLayoutManager { return adapter == null ? 0 : getItemsHeight(adapter, adapter.getItemCount()); } + @Override + protected void calculateExtraLayoutSpace(RecyclerView.State state, int[] extraLayoutSpace) { + super.calculateExtraLayoutSpace(state, extraLayoutSpace); + @Px int extraSpacePx = (int) (getHeight() * EXTRA_BOTTOM_SPACE_BY_HEIGHT_PERCENT); + extraLayoutSpace[1] = Math.max(extraLayoutSpace[1], extraSpacePx); + } + /** * Returns the sum of the height, in pixels, of this list adapter's items from index * 0 (inclusive) until {@code untilIndex} (exclusive). If untilIndex is same as the itemCount, diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java index f73347a7b2..e2f1c04ced 100644 --- a/src/com/android/launcher3/views/AbstractSlideInView.java +++ b/src/com/android/launcher3/views/AbstractSlideInView.java @@ -17,15 +17,20 @@ package com.android.launcher3.views; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.allapps.AllAppsTransitionController.REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; +import static com.android.launcher3.util.ScrollableLayoutManager.PREDICTIVE_BACK_MIN_SCALE; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Property; import android.view.MotionEvent; @@ -33,10 +38,13 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; +import androidx.annotation.FloatRange; import androidx.annotation.Nullable; +import androidx.annotation.Px; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.SingleAxisSwipeDetector; @@ -85,6 +93,10 @@ public abstract class AbstractSlideInView protected @Nullable OnCloseListener mOnCloseBeginListener; protected List mOnCloseListeners = new ArrayList<>(); + private final AnimatedFloat mSlidInViewScale = new AnimatedFloat(this::onScaleProgressChanged); + private boolean mIsBackProgressing; + @Nullable private Drawable mContentBackground; + public AbstractSlideInView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mActivityContext = ActivityContext.lookupContext(context); @@ -105,6 +117,10 @@ public abstract class AbstractSlideInView mColorScrim = scrimColor != -1 ? createColorScrim(context, scrimColor) : null; } + protected void setContentBackground(Drawable drawable) { + mContentBackground = drawable; + } + protected void attachToContainer() { if (mColorScrim != null) { getPopupContainer().addView(mColorScrim); @@ -132,6 +148,7 @@ public abstract class AbstractSlideInView if (mColorScrim != null) { mColorScrim.setAlpha(1 - mTranslationShift); } + invalidate(); } @Override @@ -161,6 +178,68 @@ public abstract class AbstractSlideInView return true; } + @Override + public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) { + super.onBackProgressed(progress); + float deceleratedProgress = + Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress); + mIsBackProgressing = progress > 0f; + mSlidInViewScale.updateValue(PREDICTIVE_BACK_MIN_SCALE + + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1 - deceleratedProgress)); + } + + private void onScaleProgressChanged() { + float scaleProgress = mSlidInViewScale.value; + SCALE_PROPERTY.set(this, scaleProgress); + setClipChildren(!mIsBackProgressing); + mContent.setClipChildren(!mIsBackProgressing); + invalidate(); + } + + @Override + public void onBackInvoked() { + super.onBackInvoked(); + animateSlideInViewToNoScale(); + } + + @Override + public void onBackCancelled() { + super.onBackCancelled(); + animateSlideInViewToNoScale(); + } + + protected void animateSlideInViewToNoScale() { + mSlidInViewScale.animateToValue(1f) + .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS) + .start(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + drawScaledBackground(canvas); + super.dispatchDraw(canvas); + } + + /** Draw scaled background during predictive back animation. */ + protected void drawScaledBackground(Canvas canvas) { + if (mContentBackground == null) { + return; + } + mContentBackground.setBounds( + mContent.getLeft(), + mContent.getTop() + (int) mContent.getTranslationY(), + mContent.getRight(), + mContent.getBottom() + (mIsBackProgressing ? getBottomOffsetPx() : 0)); + mContentBackground.draw(canvas); + } + + /** Return extra space revealed during predictive back animation. */ + @Px + protected int getBottomOffsetPx() { + return (int) (getMeasuredHeight() + * (1 - PREDICTIVE_BACK_MIN_SCALE) / 2); + } + /** * Returns {@code true} if the touch event is over the visible area of the bottom sheet. * diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index bf521cc7eb..4099302c07 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -113,6 +113,7 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { } mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize( R.dimen.widget_cell_horizontal_padding); + setContentBackground(getContext().getDrawable(R.drawable.bg_rounded_corner_bottom_sheet)); } @Override diff --git a/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java b/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java index 7f24905f88..5b1da5b368 100644 --- a/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java @@ -33,9 +33,4 @@ public class WidgetListSpaceEntry extends WidgetsListBaseEntry { Collections.EMPTY_LIST); mPkgItem.title = ""; } - - @Override - public int getRank() { - return RANK_WIDGETS_TOP_SPACE; - } } diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java index f09f4c6b5d..0003b762c2 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java @@ -16,16 +16,11 @@ package com.android.launcher3.widget.model; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; - import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.widget.WidgetItemComparator; -import java.lang.annotation.Retention; import java.util.List; import java.util.stream.Collectors; @@ -48,23 +43,4 @@ public abstract class WidgetsListBaseEntry { this.mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList()); } - - /** - * Returns the ranking of this entry in the - * {@link com.android.launcher3.widget.picker.WidgetsListAdapter}. - * - *

Entries with smaller value should be shown first. See - * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter} for more details. - */ - @Rank - public abstract int getRank(); - - @Retention(SOURCE) - @IntDef({RANK_WIDGETS_TOP_SPACE, RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT}) - public @interface Rank { - } - - public static final int RANK_WIDGETS_TOP_SPACE = 1; - public static final int RANK_WIDGETS_LIST_HEADER = 2; - public static final int RANK_WIDGETS_LIST_CONTENT = 3; } diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java index 73b17f1d4f..626e0b91c1 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java @@ -61,12 +61,6 @@ public final class WidgetsListContentEntry extends WidgetsListBaseEntry { + mMaxSpanSizeInCells; } - @Override - @Rank - public int getRank() { - return RANK_WIDGETS_LIST_CONTENT; - } - /** * Returns a copy of this {@link WidgetsListContentEntry} with updated * {@param maxSpanSizeInCells}. diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java index bb0cf9284e..68f18aebf4 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java @@ -85,12 +85,6 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { return "Header:" + mPkgItem.packageName + ":" + mWidgets.size(); } - @Override - @Rank - public int getRank() { - return RANK_WIDGETS_LIST_HEADER; - } - public boolean isSearchEntry() { return mIsSearchEntry; } diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffCallback.java b/src/com/android/launcher3/widget/picker/WidgetsDiffCallback.java new file mode 100644 index 0000000000..e610ea9a94 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsDiffCallback.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.picker; + +import androidx.recyclerview.widget.DiffUtil.Callback; + +import com.android.launcher3.widget.model.WidgetsListBaseEntry; + +import java.util.List; + +/** + * DiffUtil callback to compare widgets + */ +public class WidgetsDiffCallback extends Callback { + + private final List mOldEntries; + private final List mNewEntries; + + public WidgetsDiffCallback( + List oldEntries, + List newEntries) { + mOldEntries = oldEntries; + mNewEntries = newEntries; + } + + @Override + public int getOldListSize() { + return mOldEntries.size(); + } + + @Override + public int getNewListSize() { + return mNewEntries.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + // Items are same if they point to the same package entry + WidgetsListBaseEntry oldItem = mOldEntries.get(oldItemPosition); + WidgetsListBaseEntry newItem = mNewEntries.get(newItemPosition); + return oldItem.getClass().equals(newItem.getClass()) + && oldItem.mPkgItem.equals(newItem.mPkgItem); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + // Always update all entries since the icon may have changed + return false; + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java deleted file mode 100644 index d09fe84e7b..0000000000 --- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3.widget.picker; - -import android.util.Log; - -import androidx.recyclerview.widget.RecyclerView; - -import com.android.launcher3.icons.IconCache; -import com.android.launcher3.model.data.PackageItemInfo; -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.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/** - * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter} - * methods accordingly. - */ -public class WidgetsDiffReporter { - private static final boolean DEBUG = false; - private static final String TAG = "WidgetsDiffReporter"; - - private final IconCache mIconCache; - private final RecyclerView.Adapter mListener; - - public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) { - mIconCache = iconCache; - mListener = listener; - } - - /** - * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the - * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods. - */ - public void process(ArrayList currentEntries, - List newEntries, - WidgetListBaseRowEntryComparator comparator) { - if (DEBUG) { - Log.d(TAG, "process oldEntries#=" + currentEntries.size() - + " newEntries#=" + newEntries.size()); - } - // Early exit if either of the list is empty - if (currentEntries.isEmpty() || newEntries.isEmpty()) { - // Skip if both list are empty. - // On rotation, we open the widget tray with empty. Then try to fetch the list again - // when the animation completes (which still gives empty). And we get the final result - // when the bind actually completes. - if (currentEntries.size() != newEntries.size()) { - currentEntries.clear(); - currentEntries.addAll(newEntries); - mListener.notifyDataSetChanged(); - } - return; - } - ArrayList orgEntries = - (ArrayList) currentEntries.clone(); - Iterator orgIter = orgEntries.iterator(); - Iterator newIter = newEntries.iterator(); - - WidgetsListBaseEntry orgRowEntry = orgIter.next(); - WidgetsListBaseEntry newRowEntry = newIter.next(); - - do { - int diff = compareAppNameAndType(orgRowEntry, newRowEntry, comparator); - if (DEBUG) { - Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)", - diff, orgRowEntry != null ? orgRowEntry.toString() : null, - newRowEntry != null ? newRowEntry.toString() : null)); - } - int index = -1; - if (diff < 0) { - index = currentEntries.indexOf(orgRowEntry); - mListener.notifyItemRemoved(index); - if (DEBUG) { - Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index, - orgRowEntry.mTitleSectionName)); - } - currentEntries.remove(index); - orgRowEntry = orgIter.hasNext() ? orgIter.next() : null; - } else if (diff > 0) { - index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry) - : currentEntries.size(); - currentEntries.add(index, newRowEntry); - if (DEBUG) { - Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index, - newRowEntry.mTitleSectionName)); - } - newRowEntry = newIter.hasNext() ? newIter.next() : null; - mListener.notifyItemInserted(index); - - } else { - // same app name & type but, - // did the icon, title, etc, change? - // or did the header view changed due to user interactions? - // or did the widget size and desc, span, etc change? - if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem) - || hasHeaderUpdated(orgRowEntry, newRowEntry) - || hasWidgetsListContentChanged(orgRowEntry, newRowEntry)) { - index = currentEntries.indexOf(orgRowEntry); - currentEntries.set(index, newRowEntry); - mListener.notifyItemChanged(index); - if (DEBUG) { - Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index, - newRowEntry.mTitleSectionName)); - } - } - orgRowEntry = orgIter.hasNext() ? orgIter.next() : null; - newRowEntry = newIter.hasNext() ? newIter.next() : null; - } - } while(orgRowEntry != null || newRowEntry != null); - } - - /** - * Compares the app name and then entry type for the given {@link WidgetsListBaseEntry}s. - * - * @Return 0 if both entries' order is the same. Negative integer if {@code newRowEntry} should - * order before {@code orgRowEntry}. Positive integer if {@code orgRowEntry} should - * order before {@code newRowEntry}. - */ - private int compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow, - WidgetListBaseRowEntryComparator comparator) { - if (curRow == null && newRow == null) { - throw new IllegalStateException( - "Cannot compare PackageItemInfo if both rows are null."); - } - - if (curRow == null && newRow != null) { - return 1; // new row needs to be inserted - } else if (curRow != null && newRow == null) { - return -1; // old row needs to be deleted - } - int diff = comparator.compare(curRow, newRow); - if (diff == 0) { - return newRow.getRank() - curRow.getRank(); - } - return diff; - } - - /** - * Returns {@code true} if both {@code curRow} & {@code newRow} are - * {@link WidgetsListContentEntry}s with a different list or arrangement of widgets. - */ - private boolean hasWidgetsListContentChanged(WidgetsListBaseEntry curRow, - WidgetsListBaseEntry newRow) { - if (!(curRow instanceof WidgetsListContentEntry) - || !(newRow instanceof WidgetsListContentEntry)) { - return false; - } - return !curRow.equals(newRow); - } - - /** - * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has - * been changed due to user interactions. - */ - private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) { - if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) { - // Always refresh search header entries to reset rounded corners in their view holder. - return !curRow.equals(newRow) || ((WidgetsListHeaderEntry) curRow).isSearchEntry(); - } - return false; - } - - private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) { - return curInfo.bitmap.icon.equals(newInfo.bitmap.icon) - && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user); - } -} diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 2ce400ed68..545e661b8a 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -17,9 +17,7 @@ package com.android.launcher3.widget.picker; import static android.view.View.MeasureSpec.makeMeasureSpec; -import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; -import static com.android.launcher3.allapps.AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE; import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; @@ -31,6 +29,7 @@ import android.content.Context; import android.content.pm.LauncherApps; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Outline; import android.graphics.Rect; import android.os.Process; import android.os.UserHandle; @@ -42,6 +41,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.WindowInsets; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -59,10 +59,8 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Utilities; -import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.model.UserManagerState; @@ -170,6 +168,18 @@ public class WidgetsFullSheet extends BaseWidgetSheet } }; + private final ViewOutlineProvider mViewOutlineProvider = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRect( + 0, + 0, + view.getMeasuredWidth(), + view.getMeasuredHeight() + getBottomOffsetPx() + ); + } + }; + private final int mTabsHeight; private final int mWidgetSheetContentHorizontalPadding; @@ -195,6 +205,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet private int mOrientation; private @Nullable WidgetsRecyclerView mCurrentTouchEventRecyclerView; + private RecyclerViewFastScroller mFastScroller; + public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); DeviceProfile dp = Launcher.getLauncher(context).getDeviceProfile(); @@ -213,6 +225,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mUserManagerState.init(UserCache.INSTANCE.get(context), context.getSystemService(UserManager.class)); + setContentBackground(getContext().getDrawable(R.drawable.bg_widgets_full_sheet)); } public WidgetsFullSheet(Context context, AttributeSet attrs) { @@ -224,6 +237,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet super.onFinishInflate(); mContent = findViewById(R.id.container); + mContent.setOutlineProvider(mViewOutlineProvider); + mContent.setClipToOutline(true); + LayoutInflater layoutInflater = LayoutInflater.from(getContext()); int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view : R.layout.widgets_full_sheet_recyclerview; @@ -233,14 +249,17 @@ public class WidgetsFullSheet extends BaseWidgetSheet } layoutInflater.inflate(contentLayoutRes, mContent, true); - RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller); + mFastScroller = findViewById(R.id.fast_scroller); if (mIsTwoPane) { - fastScroller.setVisibility(GONE); + mFastScroller.setVisibility(GONE); } mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view)); mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view)); if (mHasWorkProfile) { mViewPager = findViewById(R.id.widgets_view_pager); + mViewPager.setOutlineProvider(mViewOutlineProvider); + mViewPager.setClipToOutline(true); + mViewPager.setClipChildren(false); mViewPager.initParentViews(this); mViewPager.getPageIndicator().setOnActivePageChangedListener(this); mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY); @@ -349,11 +368,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override public void onBackProgressed(@FloatRange(from = 0.0, to = 1.0) float progress) { - float deceleratedProgress = - Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress); - float scaleProgress = SWIPE_ALL_APPS_TO_HOME_MIN_SCALE - + (1 - SWIPE_ALL_APPS_TO_HOME_MIN_SCALE) * (1 - deceleratedProgress); - SCALE_PROPERTY.set(this, scaleProgress); + super.onBackProgressed(progress); + mFastScroller.setVisibility(progress > 0 ? View.INVISIBLE : View.VISIBLE); } private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { @@ -859,6 +875,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet public void onBackInvoked() { if (mIsInSearchMode) { mSearchBar.reset(); + animateSlideInViewToNoScale(); } else { super.onBackInvoked(); } @@ -943,8 +960,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet AdapterHolder(int adapterType) { mAdapterType = adapterType; Context context = getContext(); - LauncherAppState apps = LauncherAppState.getInstance(context); - HeaderChangeListener headerChangeListener = new HeaderChangeListener() { @Override public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) { @@ -975,7 +990,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet mWidgetsListAdapter = new WidgetsListAdapter( context, LayoutInflater.from(context), - apps.getIconCache(), this::getEmptySpaceHeight, /* iconClickListener= */ WidgetsFullSheet.this, /* iconLongClickListener= */ WidgetsFullSheet.this, @@ -1003,6 +1017,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet void setup(WidgetsRecyclerView recyclerView) { mWidgetsRecyclerView = recyclerView; + mWidgetsRecyclerView.setOutlineProvider(mViewOutlineProvider); + mWidgetsRecyclerView.setClipToOutline(true); + mWidgetsRecyclerView.setClipChildren(false); mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter); mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator); mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index b5ff719ed6..20b1d9b75d 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -32,13 +32,14 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.DiffUtil.DiffResult; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; import androidx.recyclerview.widget.RecyclerView.ViewHolder; import com.android.launcher3.R; -import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.recyclerview.ViewHolderBinder; import com.android.launcher3.util.LabelComparator; @@ -82,7 +83,6 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header; private final Context mContext; - private final WidgetsDiffReporter mDiffReporter; private final SparseArray mViewHolderBinders = new SparseArray<>(); private final WidgetListBaseRowEntryComparator mRowComparator = new WidgetListBaseRowEntryComparator(); @@ -102,12 +102,11 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC private int mMaxSpanSize = 4; public WidgetsListAdapter(Context context, LayoutInflater layoutInflater, - IconCache iconCache, IntSupplier emptySpaceHeightProvider, - OnClickListener iconClickListener, OnLongClickListener iconLongClickListener, + IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener, + OnLongClickListener iconLongClickListener, WidgetsFullSheet.HeaderChangeListener headerChangeListener) { mHeaderChangeListener = headerChangeListener; mContext = context; - mDiffReporter = new WidgetsDiffReporter(iconCache, this); mViewHolderBinders.put( VIEW_TYPE_WIDGETS_LIST, @@ -205,7 +204,11 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC }) .collect(Collectors.toList()); - mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator); + DiffResult diffResult = DiffUtil.calculateDiff( + new WidgetsDiffCallback(mVisibleEntries, newVisibleEntries), false); + mVisibleEntries.clear(); + mVisibleEntries.addAll(newVisibleEntries); + diffResult.dispatchUpdatesTo(this); if (mPendingClickHeader != null) { // Get the position for the clicked header after adjusting the visible entries. The diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java index f4903338d1..1b743e8bdc 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java @@ -40,7 +40,6 @@ import com.android.launcher3.widget.WidgetSections; 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.picker.WidgetsDiffReporter; import java.util.ArrayList; import java.util.Arrays; @@ -73,8 +72,7 @@ public class WidgetsModel { /** * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row * are sorted (based on label and user), but the overall list of - * {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using - * {@link WidgetsDiffReporter} + * {@link WidgetsListBaseEntry}s is not sorted. * * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) */ diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java deleted file mode 100644 index 8c87957530..0000000000 --- a/tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * 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.picker; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; - -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.UserHandle; - -import androidx.recyclerview.widget.RecyclerView; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.InvariantDeviceProfile; -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.widget.LauncherAppWidgetProviderInfo; -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.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public final class WidgetsDiffReporterTest { - private static final String TEST_PACKAGE_PREFIX = "com.android.test"; - private static final WidgetListBaseRowEntryComparator COMPARATOR = - new WidgetListBaseRowEntryComparator(); - - @Mock private IconCache mIconCache; - @Mock private RecyclerView.Adapter mAdapter; - - private InvariantDeviceProfile mTestProfile; - private WidgetsDiffReporter mWidgetsDiffReporter; - private Context mContext; - private WidgetsListHeaderEntry mHeaderA; - private WidgetsListHeaderEntry mHeaderB; - private WidgetsListHeaderEntry mHeaderC; - private WidgetsListHeaderEntry mHeaderD; - private WidgetsListHeaderEntry mHeaderE; - private WidgetsListContentEntry mContentC; - private WidgetsListContentEntry mContentE; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mTestProfile = new InvariantDeviceProfile(); - mTestProfile.numRows = 5; - mTestProfile.numColumns = 5; - - doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0)) - .getComponent().getPackageName()) - .when(mIconCache).getTitleNoCache(any()); - - mContext = getApplicationContext(); - mWidgetsDiffReporter = new WidgetsDiffReporter(mIconCache, mAdapter); - mHeaderA = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", - /* appName= */ "A", /* numOfWidgets= */ 3); - mHeaderB = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", - /* appName= */ "B", /* numOfWidgets= */ 3); - mHeaderC = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", - /* appName= */ "C", /* numOfWidgets= */ 3); - mContentC = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "C", - /* appName= */ "C", /* numOfWidgets= */ 3); - mHeaderD = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "D", - /* appName= */ "D", /* numOfWidgets= */ 3); - mHeaderE = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "E", - /* appName= */ "E", /* numOfWidgets= */ 3); - mContentE = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "E", - /* appName= */ "E", /* numOfWidgets= */ 3); - } - - @Test - public void listNotChanged_shouldNotInvokeAnyCallbacks() { - // GIVEN the current list has app headers [A, B, C]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mHeaderC)); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, currentList, COMPARATOR); - - // THEN there is no adaptor callback. - verifyZeroInteractions(mAdapter); - // THEN the current list contains the same entries. - assertThat(currentList).containsExactly(mHeaderA, mHeaderB, mHeaderC); - } - - @Test - public void headersOnly_emptyListToNonEmpty_shouldInvokeNotifyDataSetChanged() { - // GIVEN the current list has app headers [A, B, C]. - ArrayList currentList = new ArrayList<>(); - - List newList = List.of( - createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", "A", 3), - createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", "B", 3), - createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", "C", 3)); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN notifyDataSetChanged is called - verify(mAdapter).notifyDataSetChanged(); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - @Test - public void headersOnly_nonEmptyToEmptyList_shouldInvokeNotifyDataSetChanged() { - // GIVEN the current list has app headers [A, B, C]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mHeaderC)); - // GIVEN the new list is empty. - List newList = List.of(); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN notifyDataSetChanged is called. - verify(mAdapter).notifyDataSetChanged(); - // THEN the current list isEmpty. - assertThat(currentList).isEmpty(); - } - - @Test - public void headersOnly_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() { - // GIVEN the current list has app headers [A, B, D]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mHeaderD)); - // GIVEN the new list has app headers [A, C, E]. - List newList = List.of(mHeaderA, mHeaderC, mHeaderE); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN "B" is removed from position 1. - verify(mAdapter).notifyItemRemoved(/* position= */ 1); - // THEN "D" is removed from position 2. - verify(mAdapter).notifyItemRemoved(/* position= */ 2); - // THEN "C" is inserted at position 1. - verify(mAdapter).notifyItemInserted(/* position= */ 1); - // THEN "E" is inserted at position 2. - verify(mAdapter).notifyItemInserted(/* position= */ 2); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - @Test - public void headersContentsMix_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() { - // GIVEN the current list has app headers [A, B, E content]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mContentE)); - // GIVEN the new list has app headers [A, C content, D]. - List newList = List.of(mHeaderA, mContentC, mHeaderD); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN "B" is removed from position 1. - verify(mAdapter).notifyItemRemoved(/* position= */ 1); - // THEN "C content" is inserted at position 1. - verify(mAdapter).notifyItemInserted(/* position= */ 1); - // THEN "D" is inserted at position 2. - verify(mAdapter).notifyItemInserted(/* position= */ 2); - // THEN "E content" is removed from position 3. - verify(mAdapter).notifyItemRemoved(/* position= */ 3); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - @Test - public void headersContentsMix_userInteractWithHeader_shouldInvokeCorrectCallbacks() { - // GIVEN the current list has app headers [A, B, E content]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mContentE)); - // GIVEN the new list has app headers [A, B, E content] and the user has interacted with B. - List newList = - List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN notify "B" has been changed. - verify(mAdapter).notifyItemChanged(/* position= */ 1); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - @Test - public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() { - // GIVEN the current list has app headers [A, B, E content]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mContentE)); - // GIVEN the new list has one of the headers widgets list modified. - List newList = List.of( - WidgetsListHeaderEntry.create( - mHeaderA.mPkgItem, mHeaderA.mTitleSectionName, - mHeaderA.mWidgets.subList(0, 1)), - mHeaderB, mContentE); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN notify "A" has been changed. - verify(mAdapter).notifyItemChanged(/* position= */ 0); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - @Test - public void headersContentsMix_contentMaxSpanSizeModified_shouldInvokeCorrectCallbacks() { - // GIVEN the current list has app headers [A, B, E content]. - ArrayList currentList = new ArrayList<>( - List.of(mHeaderA, mHeaderB, mContentE)); - // GIVEN the new list has max span size in "E content" modified. - List newList = List.of( - mHeaderA, - mHeaderB, - new WidgetsListContentEntry( - mContentE.mPkgItem, - mContentE.mTitleSectionName, - mContentE.mWidgets, - mContentE.getMaxSpanSizeInCells() + 1)); - - // WHEN computing the list difference. - mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); - - // THEN notify "E content" has been changed. - verify(mAdapter).notifyItemChanged(/* position= */ 2); - // THEN the current list contains all elements from the new list. - assertThat(currentList).containsExactlyElementsIn(newList); - } - - - private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName, - int numOfWidgets) { - List widgetItems = generateWidgetItems(packageName, numOfWidgets); - PackageItemInfo pInfo = createPackageItemInfo(packageName, appName, - widgetItems.get(0).user); - - return WidgetsListHeaderEntry.create(pInfo, /* titleSectionName= */ "", widgetItems); - } - - private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName, - int numOfWidgets) { - List widgetItems = generateWidgetItems(packageName, numOfWidgets); - PackageItemInfo pInfo = createPackageItemInfo(packageName, appName, - widgetItems.get(0).user); - - return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems); - } - - private PackageItemInfo createPackageItemInfo(String packageName, String appName, - UserHandle userHandle) { - PackageItemInfo pInfo = new PackageItemInfo(packageName, userHandle); - pInfo.title = appName; - pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); - return pInfo; - } - - private List generateWidgetItems(String packageName, int numOfWidgets) { - ArrayList widgetItems = new ArrayList<>(); - for (int i = 0; i < numOfWidgets; i++) { - ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i); - AppWidgetProviderInfo widgetInfo = createAppWidgetProviderInfo(cn); - - WidgetItem widgetItem = new WidgetItem( - LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), - mTestProfile, mIconCache); - widgetItems.add(widgetItem); - } - return widgetItems; - } -} diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java deleted file mode 100644 index 0044d046b1..0000000000 --- a/tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.widget.picker; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.verify; - -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Process; -import android.os.UserHandle; -import android.view.LayoutInflater; - -import androidx.recyclerview.widget.RecyclerView; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.InvariantDeviceProfile; -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.ActivityContextWrapper; -import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.util.WidgetUtils; -import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.model.WidgetsListBaseEntry; -import com.android.launcher3.widget.model.WidgetsListContentEntry; -import com.android.launcher3.widget.model.WidgetsListHeaderEntry; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -/** - * Unit tests for WidgetsListAdapter - * Note that all indices matching are shifted by 1 to account for the empty space at the start. - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public final class WidgetsListAdapterTest { - private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test"; - - @Mock private LayoutInflater mMockLayoutInflater; - @Mock private RecyclerView.AdapterDataObserver mListener; - @Mock private IconCache mIconCache; - - private WidgetsListAdapter mAdapter; - private InvariantDeviceProfile mTestProfile; - private UserHandle mUserHandle; - private Context mContext; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = new ActivityContextWrapper(getApplicationContext()); - mTestProfile = new InvariantDeviceProfile(); - mTestProfile.numRows = 5; - mTestProfile.numColumns = 5; - mUserHandle = Process.myUserHandle(); - mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, - mIconCache, () -> 0, null, null, null); - mAdapter.registerAdapterDataObserver(mListener); - - doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0)) - .getComponent().getPackageName()) - .when(mIconCache).getTitleNoCache(any()); - } - - @Test - public void setWidgets_shouldNotifyDataSetChanged() { - mAdapter.setWidgets(generateSampleMap(1)); - - verify(mListener).onChanged(); - } - - @Test - public void setWidgets_withItemInserted_shouldNotifyItemInserted() { - mAdapter.setWidgets(generateSampleMap(1)); - mAdapter.setWidgets(generateSampleMap(2)); - - verify(mListener).onItemRangeInserted(eq(2), eq(1)); - } - - @Test - public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() { - mAdapter.setWidgets(generateSampleMap(2)); - mAdapter.setWidgets(generateSampleMap(1)); - - verify(mListener).onItemRangeRemoved(eq(2), eq(1)); - } - - @Test - public void setWidgets_appIconChanged_shouldNotifyItemChanged() { - mAdapter.setWidgets(generateSampleMap(1)); - mAdapter.setWidgets(generateSampleMap(1)); - - verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull()); - } - - @Test - public void headerClick_expanded_shouldNotifyItemChange() { - // GIVEN a list of widgets entries: - // [com.google.test0, com.google.test0 content, - // com.google.test1, com.google.test1 content, - // com.google.test2, com.google.test2 content] - // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2]. - mAdapter.setWidgets(generateSampleMap(3)); - - // WHEN com.google.test.1 header is expanded. - mAdapter.onHeaderClicked(/* showWidgets= */ true, - new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); - - // THEN the visible entries list becomes: - // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2] - // com.google.test.1 content is inserted into position 2. - verify(mListener).onItemRangeInserted(eq(3), eq(1)); - } - - @Test - public void setWidgets_expandedApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() { - // GIVEN the adapter was first populated with com.google.test0 & com.google.test1. Each app - // has one widget. - ArrayList allEntries = generateSampleMap(2); - mAdapter.setWidgets(allEntries); - // GIVEN test com.google.test1 is expanded. - // Visible entries in the adapter are: - // [com.google.test0, com.google.test1, com.google.test1 content] - mAdapter.onHeaderClicked(/* showWidgets= */ true, - new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); - Mockito.reset(mListener); - - // WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets - // now. - WidgetsListContentEntry testPackage1ContentEntry = - (WidgetsListContentEntry) allEntries.get(3); - WidgetItem widgetItem = testPackage1ContentEntry.mWidgets.get(0); - WidgetsListContentEntry newTestPackage1ContentEntry = new WidgetsListContentEntry( - testPackage1ContentEntry.mPkgItem, - testPackage1ContentEntry.mTitleSectionName, List.of(widgetItem, widgetItem)); - allEntries.set(3, newTestPackage1ContentEntry); - mAdapter.setWidgets(allEntries); - - // THEN the onItemRangeChanged is invoked for "com.google.test1 content" at index 2. - verify(mListener).onItemRangeChanged(eq(3), eq(1), isNull()); - } - - @Test - public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() { - // GIVEN a widgets entry list: - // Index: 0| 1 | 2| 3 | 4| 5 | 6| 7 | 8| 9 | - // [A, A content, B, B content, C, C content, D, D content, E, E content] - List allAppsWithWidgets = generateSampleMap(5); - // GIVEN the current widgets list consist of [A, A content, B, B content, E, E content]. - // GIVEN the visible widgets list consist of [A, B, E] - List currentList = List.of( - // A & A content - allAppsWithWidgets.get(0), allAppsWithWidgets.get(1), - // B & B content - allAppsWithWidgets.get(2), allAppsWithWidgets.get(3), - // E & E content - allAppsWithWidgets.get(8), allAppsWithWidgets.get(9)); - mAdapter.setWidgets(currentList); - - // WHEN the widgets list is updated to [A, A content, C, C content, D, D content]. - // WHEN the visible widgets list is updated to [A, C, D]. - List newList = List.of( - // A & A content - allAppsWithWidgets.get(0), allAppsWithWidgets.get(1), - // C & C content - allAppsWithWidgets.get(4), allAppsWithWidgets.get(5), - // D & D content - allAppsWithWidgets.get(6), allAppsWithWidgets.get(7)); - mAdapter.setWidgets(newList); - - // Account for 1st items as empty space - // Computation logic | [Intermediate list during computation] - // THEN B <> C < 0, removed B from index 1 | [A, E] - verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1); - // THEN E <> C > 0, C inserted to index 1 | [A, C, E] - verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1); - // THEN E <> D > 0, D inserted to index 2 | [A, C, D, E] - verify(mListener).onItemRangeInserted(/* positionStart= */ 3, /* itemCount= */ 1); - // THEN E <> null = -1, E deleted from index 3 | [A, C, D] - verify(mListener).onItemRangeRemoved(/* positionStart= */ 4, /* itemCount= */ 1); - } - - @Test - public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() { - // GIVEN a list of widgets entries: - // [Empty item - // com.google.test0, - // com.google.test0 content, - // com.google.test1, - // com.google.test1 content, - // com.google.test2, - // com.google.test2 content] - // The visible widgets entries: - // [Empty item, - // com.google.test0, - // com.google.test1, - // com.google.test2]. - ArrayList allEntries = generateSampleMap(3); - mAdapter.setWidgetsOnSearch(allEntries); - // GIVEN com.google.test.1 header is expanded. The visible entries list becomes: - // [Empty item, com.google.test0, com.google.test1, com.google.test1 content, - // com.google.test2] - mAdapter.onHeaderClicked(/* showWidgets= */ true, - new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); - Mockito.reset(mListener); - - // WHEN same widget entries are set again. - mAdapter.setWidgetsOnSearch(allEntries); - - // THEN expanded app is reset and the visible entries list becomes: - // [Empty item, com.google.test0, com.google.test1, com.google.test2] - verify(mListener).onItemRangeChanged(eq(2), eq(1), isNull()); - verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1); - } - - /** - * Generates a list of sample widget entries. - * - *

Each sample app has 1 widget only. An app is represented by 2 entries, - * {@link WidgetsListHeaderEntry} & {@link WidgetsListContentEntry}. Only - * {@link WidgetsListHeaderEntry} is always visible in the {@link WidgetsListAdapter}. - * {@link WidgetsListContentEntry} is only shown upon clicking the corresponding app's - * {@link WidgetsListHeaderEntry}. Only at most one {@link WidgetsListContentEntry} is shown at - * a time. - * - * @param num the number of apps that have widgets. - */ - private ArrayList generateSampleMap(int num) { - ArrayList result = new ArrayList<>(); - if (num <= 0) return result; - - for (int i = 0; i < num; i++) { - String packageName = TEST_PACKAGE_PLACEHOLDER + i; - - List widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1); - - PackageItemInfo pInfo = new PackageItemInfo(packageName, widgetItems.get(0).user); - pInfo.title = pInfo.packageName; - pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); - - result.add(WidgetsListHeaderEntry.create( - pInfo, /* titleSectionName= */ "", widgetItems)); - result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems)); - } - - return result; - } - - private List generateWidgetItems(String packageName, int numOfWidgets) { - ArrayList widgetItems = new ArrayList<>(); - for (int i = 0; i < numOfWidgets; i++) { - ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i); - AppWidgetProviderInfo widgetInfo = WidgetUtils.createAppWidgetProviderInfo(cn); - - widgetItems.add(new WidgetItem( - LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), - mTestProfile, mIconCache)); - } - return widgetItems; - } -}