Merge "Using DiffUtil for calculating widget diff instead of a custom implementation" into tm-qpr-dev am: cc10ed5532

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/21381953

Change-Id: I42080a592bef63d0cb7e55189a1a6249f331220a
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Sunny Goyal
2023-02-17 00:02:41 +00:00
committed by Automerger Merge Worker
11 changed files with 74 additions and 844 deletions

View File

@@ -33,9 +33,4 @@ public class WidgetListSpaceEntry extends WidgetsListBaseEntry {
Collections.EMPTY_LIST);
mPkgItem.title = "";
}
@Override
public int getRank() {
return RANK_WIDGETS_TOP_SPACE;
}
}

View File

@@ -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;
}

View File

@@ -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}.

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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)
*/

View File

@@ -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;
}
}

View File

@@ -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;
}
}