Snap for 9626528 from cc7fc9f5cc to udc-release
Change-Id: Ie8647aa738ce0c6582157b59e595917c3f4691ca
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1624,10 +1624,13 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
|
||||
|
||||
// If we are entering Overview as a result of initiating a split from somewhere else
|
||||
// (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail.
|
||||
int stagedTaskIdToBeRemovedFromGrid = mSplitSelectSource != null
|
||||
? mSplitSelectSource.alreadyRunningTaskId
|
||||
: INVALID_TASK_ID;
|
||||
|
||||
int stagedTaskIdToBeRemovedFromGrid;
|
||||
if (mSplitSelectSource != null) {
|
||||
stagedTaskIdToBeRemovedFromGrid = mSplitSelectSource.alreadyRunningTaskId;
|
||||
updateCurrentTaskActionsVisibility();
|
||||
} else {
|
||||
stagedTaskIdToBeRemovedFromGrid = INVALID_TASK_ID;
|
||||
}
|
||||
// update the map of instance counts
|
||||
mFilterState.updateInstanceCountMap(taskGroups);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
android:id="@+id/widgets_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_rounded_corner_bottom_sheet"
|
||||
android:paddingTop="@dimen/bottom_sheet_handle_margin"
|
||||
android:orientation="vertical">
|
||||
<View
|
||||
|
||||
@@ -25,7 +25,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">
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/collapse_handle"
|
||||
android:paddingBottom="0dp"
|
||||
android:clipToOutline="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
android:id="@+id/search_and_recommendations_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToOutline="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
android:layout_below="@id/collapse_handle"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
|
||||
android:clipToOutline="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
android:id="@+id/search_and_recommendations_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToOutline="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -1315,23 +1315,29 @@ public class DeviceProfile {
|
||||
hotseatBarSizePx - hotseatBarBottomPadding - hotseatCellHeightPx;
|
||||
|
||||
int hotseatWidth = getHotseatRequiredWidth();
|
||||
int leftSpacing = (availableWidthPx - hotseatWidth) / 2;
|
||||
int rightSpacing = leftSpacing;
|
||||
int startSpacing;
|
||||
int endSpacing;
|
||||
// Hotseat aligns to the left with nav buttons
|
||||
if (hotseatBarEndOffset > 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,
|
||||
|
||||
@@ -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<T extends Context & ActivityContext>
|
||||
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,
|
||||
|
||||
@@ -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<T extends Context & ActivityContext> 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.
|
||||
|
||||
@@ -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<LauncherState>, 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<T extends Context & ActivityContext>
|
||||
protected @Nullable OnCloseListener mOnCloseBeginListener;
|
||||
protected List<OnCloseListener> 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<T extends Context & ActivityContext>
|
||||
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<T extends Context & ActivityContext>
|
||||
if (mColorScrim != null) {
|
||||
mColorScrim.setAlpha(1 - mTranslationShift);
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,6 +178,68 @@ public abstract class AbstractSlideInView<T extends Context & ActivityContext>
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,9 +33,4 @@ public class WidgetListSpaceEntry extends WidgetsListBaseEntry {
|
||||
Collections.EMPTY_LIST);
|
||||
mPkgItem.title = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRank() {
|
||||
return RANK_WIDGETS_TOP_SPACE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<WidgetsListBaseEntry> mOldEntries;
|
||||
private final List<WidgetsListBaseEntry> mNewEntries;
|
||||
|
||||
public WidgetsDiffCallback(
|
||||
List<WidgetsListBaseEntry> oldEntries,
|
||||
List<WidgetsListBaseEntry> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<WidgetsListBaseEntry> currentEntries,
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> orgEntries =
|
||||
(ArrayList<WidgetsListBaseEntry>) currentEntries.clone();
|
||||
Iterator<WidgetsListBaseEntry> orgIter = orgEntries.iterator();
|
||||
Iterator<WidgetsListBaseEntry> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ViewHolder> 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<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
|
||||
private final WidgetListBaseRowEntryComparator mRowComparator =
|
||||
new WidgetListBaseRowEntryComparator();
|
||||
@@ -102,12 +102,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> 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<ViewHolder> 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
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>();
|
||||
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mHeaderC));
|
||||
// GIVEN the new list is empty.
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mHeaderD));
|
||||
// GIVEN the new list has app headers [A, C, E].
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has app headers [A, C content, D].
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has one of the headers widgets list modified.
|
||||
List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has max span size in "E content" modified.
|
||||
List<WidgetsListBaseEntry> 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<WidgetItem> 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<WidgetItem> 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<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
|
||||
ArrayList<WidgetItem> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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.
|
||||
*
|
||||
* <p>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<WidgetsListBaseEntry> generateSampleMap(int num) {
|
||||
ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
|
||||
if (num <= 0) return result;
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
String packageName = TEST_PACKAGE_PLACEHOLDER + i;
|
||||
|
||||
List<WidgetItem> 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<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
|
||||
ArrayList<WidgetItem> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user