Snap for 9626528 from cc7fc9f5cc to udc-release

Change-Id: Ie8647aa738ce0c6582157b59e595917c3f4691ca
This commit is contained in:
Android Build Coastguard Worker
2023-02-18 06:32:03 +00:00
28 changed files with 236 additions and 891 deletions
@@ -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
-1
View File
@@ -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
+15 -9
View File
@@ -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;
}
}