diff --git a/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java b/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java index 7f24905f88..5b1da5b368 100644 --- a/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetListSpaceEntry.java @@ -33,9 +33,4 @@ public class WidgetListSpaceEntry extends WidgetsListBaseEntry { Collections.EMPTY_LIST); mPkgItem.title = ""; } - - @Override - public int getRank() { - return RANK_WIDGETS_TOP_SPACE; - } } diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java index f09f4c6b5d..0003b762c2 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java @@ -16,16 +16,11 @@ package com.android.launcher3.widget.model; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; - import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.widget.WidgetItemComparator; -import java.lang.annotation.Retention; import java.util.List; import java.util.stream.Collectors; @@ -48,23 +43,4 @@ public abstract class WidgetsListBaseEntry { this.mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList()); } - - /** - * Returns the ranking of this entry in the - * {@link com.android.launcher3.widget.picker.WidgetsListAdapter}. - * - *

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

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