mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 02:38:20 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -32,13 +32,14 @@ import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.DiffUtil.DiffResult;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.recyclerview.ViewHolderBinder;
|
||||
import com.android.launcher3.util.LabelComparator;
|
||||
@@ -82,7 +83,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
|
||||
|
||||
private final Context mContext;
|
||||
private final WidgetsDiffReporter mDiffReporter;
|
||||
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
|
||||
private final WidgetListBaseRowEntryComparator mRowComparator =
|
||||
new WidgetListBaseRowEntryComparator();
|
||||
@@ -102,12 +102,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
private int mMaxSpanSize = 4;
|
||||
|
||||
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
|
||||
IconCache iconCache, IntSupplier emptySpaceHeightProvider,
|
||||
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,
|
||||
IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener,
|
||||
OnLongClickListener iconLongClickListener,
|
||||
WidgetsFullSheet.HeaderChangeListener headerChangeListener) {
|
||||
mHeaderChangeListener = headerChangeListener;
|
||||
mContext = context;
|
||||
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
|
||||
|
||||
mViewHolderBinders.put(
|
||||
VIEW_TYPE_WIDGETS_LIST,
|
||||
@@ -205,7 +204,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
|
||||
DiffResult diffResult = DiffUtil.calculateDiff(
|
||||
new WidgetsDiffCallback(mVisibleEntries, newVisibleEntries), false);
|
||||
mVisibleEntries.clear();
|
||||
mVisibleEntries.addAll(newVisibleEntries);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
|
||||
if (mPendingClickHeader != null) {
|
||||
// Get the position for the clicked header after adjusting the visible entries. The
|
||||
|
||||
@@ -40,7 +40,6 @@ import com.android.launcher3.widget.WidgetSections;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
|
||||
import com.android.launcher3.widget.picker.WidgetsDiffReporter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -73,8 +72,7 @@ public class WidgetsModel {
|
||||
/**
|
||||
* Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
|
||||
* are sorted (based on label and user), but the overall list of
|
||||
* {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using
|
||||
* {@link WidgetsDiffReporter}
|
||||
* {@link WidgetsListBaseEntry}s is not sorted.
|
||||
*
|
||||
* @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
|
||||
*/
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||
|
||||
import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
|
||||
import android.appwidget.AppWidgetProviderInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.ComponentWithLabel;
|
||||
import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
|
||||
import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class WidgetsDiffReporterTest {
|
||||
private static final String TEST_PACKAGE_PREFIX = "com.android.test";
|
||||
private static final WidgetListBaseRowEntryComparator COMPARATOR =
|
||||
new WidgetListBaseRowEntryComparator();
|
||||
|
||||
@Mock private IconCache mIconCache;
|
||||
@Mock private RecyclerView.Adapter mAdapter;
|
||||
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
private WidgetsDiffReporter mWidgetsDiffReporter;
|
||||
private Context mContext;
|
||||
private WidgetsListHeaderEntry mHeaderA;
|
||||
private WidgetsListHeaderEntry mHeaderB;
|
||||
private WidgetsListHeaderEntry mHeaderC;
|
||||
private WidgetsListHeaderEntry mHeaderD;
|
||||
private WidgetsListHeaderEntry mHeaderE;
|
||||
private WidgetsListContentEntry mContentC;
|
||||
private WidgetsListContentEntry mContentE;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mTestProfile = new InvariantDeviceProfile();
|
||||
mTestProfile.numRows = 5;
|
||||
mTestProfile.numColumns = 5;
|
||||
|
||||
doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
|
||||
.getComponent().getPackageName())
|
||||
.when(mIconCache).getTitleNoCache(any());
|
||||
|
||||
mContext = getApplicationContext();
|
||||
mWidgetsDiffReporter = new WidgetsDiffReporter(mIconCache, mAdapter);
|
||||
mHeaderA = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A",
|
||||
/* appName= */ "A", /* numOfWidgets= */ 3);
|
||||
mHeaderB = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B",
|
||||
/* appName= */ "B", /* numOfWidgets= */ 3);
|
||||
mHeaderC = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C",
|
||||
/* appName= */ "C", /* numOfWidgets= */ 3);
|
||||
mContentC = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "C",
|
||||
/* appName= */ "C", /* numOfWidgets= */ 3);
|
||||
mHeaderD = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "D",
|
||||
/* appName= */ "D", /* numOfWidgets= */ 3);
|
||||
mHeaderE = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "E",
|
||||
/* appName= */ "E", /* numOfWidgets= */ 3);
|
||||
mContentE = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "E",
|
||||
/* appName= */ "E", /* numOfWidgets= */ 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listNotChanged_shouldNotInvokeAnyCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, C].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mHeaderC));
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, currentList, COMPARATOR);
|
||||
|
||||
// THEN there is no adaptor callback.
|
||||
verifyZeroInteractions(mAdapter);
|
||||
// THEN the current list contains the same entries.
|
||||
assertThat(currentList).containsExactly(mHeaderA, mHeaderB, mHeaderC);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersOnly_emptyListToNonEmpty_shouldInvokeNotifyDataSetChanged() {
|
||||
// GIVEN the current list has app headers [A, B, C].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>();
|
||||
|
||||
List<WidgetsListBaseEntry> newList = List.of(
|
||||
createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", "A", 3),
|
||||
createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", "B", 3),
|
||||
createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", "C", 3));
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN notifyDataSetChanged is called
|
||||
verify(mAdapter).notifyDataSetChanged();
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersOnly_nonEmptyToEmptyList_shouldInvokeNotifyDataSetChanged() {
|
||||
// GIVEN the current list has app headers [A, B, C].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mHeaderC));
|
||||
// GIVEN the new list is empty.
|
||||
List<WidgetsListBaseEntry> newList = List.of();
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN notifyDataSetChanged is called.
|
||||
verify(mAdapter).notifyDataSetChanged();
|
||||
// THEN the current list isEmpty.
|
||||
assertThat(currentList).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersOnly_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, D].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mHeaderD));
|
||||
// GIVEN the new list has app headers [A, C, E].
|
||||
List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderC, mHeaderE);
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN "B" is removed from position 1.
|
||||
verify(mAdapter).notifyItemRemoved(/* position= */ 1);
|
||||
// THEN "D" is removed from position 2.
|
||||
verify(mAdapter).notifyItemRemoved(/* position= */ 2);
|
||||
// THEN "C" is inserted at position 1.
|
||||
verify(mAdapter).notifyItemInserted(/* position= */ 1);
|
||||
// THEN "E" is inserted at position 2.
|
||||
verify(mAdapter).notifyItemInserted(/* position= */ 2);
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersContentsMix_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, E content].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has app headers [A, C content, D].
|
||||
List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mContentC, mHeaderD);
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN "B" is removed from position 1.
|
||||
verify(mAdapter).notifyItemRemoved(/* position= */ 1);
|
||||
// THEN "C content" is inserted at position 1.
|
||||
verify(mAdapter).notifyItemInserted(/* position= */ 1);
|
||||
// THEN "D" is inserted at position 2.
|
||||
verify(mAdapter).notifyItemInserted(/* position= */ 2);
|
||||
// THEN "E content" is removed from position 3.
|
||||
verify(mAdapter).notifyItemRemoved(/* position= */ 3);
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersContentsMix_userInteractWithHeader_shouldInvokeCorrectCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, E content].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has app headers [A, B, E content] and the user has interacted with B.
|
||||
List<WidgetsListBaseEntry> newList =
|
||||
List.of(mHeaderA, mHeaderB.withWidgetListShown(), mContentE);
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN notify "B" has been changed.
|
||||
verify(mAdapter).notifyItemChanged(/* position= */ 1);
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, E content].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has one of the headers widgets list modified.
|
||||
List<WidgetsListBaseEntry> newList = List.of(
|
||||
WidgetsListHeaderEntry.create(
|
||||
mHeaderA.mPkgItem, mHeaderA.mTitleSectionName,
|
||||
mHeaderA.mWidgets.subList(0, 1)),
|
||||
mHeaderB, mContentE);
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN notify "A" has been changed.
|
||||
verify(mAdapter).notifyItemChanged(/* position= */ 0);
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersContentsMix_contentMaxSpanSizeModified_shouldInvokeCorrectCallbacks() {
|
||||
// GIVEN the current list has app headers [A, B, E content].
|
||||
ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
|
||||
List.of(mHeaderA, mHeaderB, mContentE));
|
||||
// GIVEN the new list has max span size in "E content" modified.
|
||||
List<WidgetsListBaseEntry> newList = List.of(
|
||||
mHeaderA,
|
||||
mHeaderB,
|
||||
new WidgetsListContentEntry(
|
||||
mContentE.mPkgItem,
|
||||
mContentE.mTitleSectionName,
|
||||
mContentE.mWidgets,
|
||||
mContentE.getMaxSpanSizeInCells() + 1));
|
||||
|
||||
// WHEN computing the list difference.
|
||||
mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
|
||||
|
||||
// THEN notify "E content" has been changed.
|
||||
verify(mAdapter).notifyItemChanged(/* position= */ 2);
|
||||
// THEN the current list contains all elements from the new list.
|
||||
assertThat(currentList).containsExactlyElementsIn(newList);
|
||||
}
|
||||
|
||||
|
||||
private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
|
||||
int numOfWidgets) {
|
||||
List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
|
||||
PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
|
||||
widgetItems.get(0).user);
|
||||
|
||||
return WidgetsListHeaderEntry.create(pInfo, /* titleSectionName= */ "", widgetItems);
|
||||
}
|
||||
|
||||
private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
|
||||
int numOfWidgets) {
|
||||
List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
|
||||
PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
|
||||
widgetItems.get(0).user);
|
||||
|
||||
return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
|
||||
}
|
||||
|
||||
private PackageItemInfo createPackageItemInfo(String packageName, String appName,
|
||||
UserHandle userHandle) {
|
||||
PackageItemInfo pInfo = new PackageItemInfo(packageName, userHandle);
|
||||
pInfo.title = appName;
|
||||
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
|
||||
return pInfo;
|
||||
}
|
||||
|
||||
private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
|
||||
ArrayList<WidgetItem> widgetItems = new ArrayList<>();
|
||||
for (int i = 0; i < numOfWidgets; i++) {
|
||||
ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
|
||||
AppWidgetProviderInfo widgetInfo = createAppWidgetProviderInfo(cn);
|
||||
|
||||
WidgetItem widgetItem = new WidgetItem(
|
||||
LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
|
||||
mTestProfile, mIconCache);
|
||||
widgetItems.add(widgetItem);
|
||||
}
|
||||
return widgetItems;
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isNull;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.appwidget.AppWidgetProviderInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Process;
|
||||
import android.os.UserHandle;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.launcher3.InvariantDeviceProfile;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.ComponentWithLabel;
|
||||
import com.android.launcher3.icons.IconCache;
|
||||
import com.android.launcher3.model.WidgetItem;
|
||||
import com.android.launcher3.model.data.PackageItemInfo;
|
||||
import com.android.launcher3.util.ActivityContextWrapper;
|
||||
import com.android.launcher3.util.PackageUserKey;
|
||||
import com.android.launcher3.util.WidgetUtils;
|
||||
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
|
||||
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
||||
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Unit tests for WidgetsListAdapter
|
||||
* Note that all indices matching are shifted by 1 to account for the empty space at the start.
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class WidgetsListAdapterTest {
|
||||
private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
|
||||
|
||||
@Mock private LayoutInflater mMockLayoutInflater;
|
||||
@Mock private RecyclerView.AdapterDataObserver mListener;
|
||||
@Mock private IconCache mIconCache;
|
||||
|
||||
private WidgetsListAdapter mAdapter;
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
private UserHandle mUserHandle;
|
||||
private Context mContext;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = new ActivityContextWrapper(getApplicationContext());
|
||||
mTestProfile = new InvariantDeviceProfile();
|
||||
mTestProfile.numRows = 5;
|
||||
mTestProfile.numColumns = 5;
|
||||
mUserHandle = Process.myUserHandle();
|
||||
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater,
|
||||
mIconCache, () -> 0, null, null, null);
|
||||
mAdapter.registerAdapterDataObserver(mListener);
|
||||
|
||||
doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
|
||||
.getComponent().getPackageName())
|
||||
.when(mIconCache).getTitleNoCache(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_shouldNotifyDataSetChanged() {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
|
||||
verify(mListener).onChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_withItemInserted_shouldNotifyItemInserted() {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
mAdapter.setWidgets(generateSampleMap(2));
|
||||
|
||||
verify(mListener).onItemRangeInserted(eq(2), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() {
|
||||
mAdapter.setWidgets(generateSampleMap(2));
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
|
||||
verify(mListener).onItemRangeRemoved(eq(2), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_appIconChanged_shouldNotifyItemChanged() {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
|
||||
verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerClick_expanded_shouldNotifyItemChange() {
|
||||
// GIVEN a list of widgets entries:
|
||||
// [com.google.test0, com.google.test0 content,
|
||||
// com.google.test1, com.google.test1 content,
|
||||
// com.google.test2, com.google.test2 content]
|
||||
// The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
|
||||
mAdapter.setWidgets(generateSampleMap(3));
|
||||
|
||||
// WHEN com.google.test.1 header is expanded.
|
||||
mAdapter.onHeaderClicked(/* showWidgets= */ true,
|
||||
new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
|
||||
|
||||
// THEN the visible entries list becomes:
|
||||
// [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
|
||||
// com.google.test.1 content is inserted into position 2.
|
||||
verify(mListener).onItemRangeInserted(eq(3), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_expandedApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() {
|
||||
// GIVEN the adapter was first populated with com.google.test0 & com.google.test1. Each app
|
||||
// has one widget.
|
||||
ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
|
||||
mAdapter.setWidgets(allEntries);
|
||||
// GIVEN test com.google.test1 is expanded.
|
||||
// Visible entries in the adapter are:
|
||||
// [com.google.test0, com.google.test1, com.google.test1 content]
|
||||
mAdapter.onHeaderClicked(/* showWidgets= */ true,
|
||||
new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
|
||||
Mockito.reset(mListener);
|
||||
|
||||
// WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets
|
||||
// now.
|
||||
WidgetsListContentEntry testPackage1ContentEntry =
|
||||
(WidgetsListContentEntry) allEntries.get(3);
|
||||
WidgetItem widgetItem = testPackage1ContentEntry.mWidgets.get(0);
|
||||
WidgetsListContentEntry newTestPackage1ContentEntry = new WidgetsListContentEntry(
|
||||
testPackage1ContentEntry.mPkgItem,
|
||||
testPackage1ContentEntry.mTitleSectionName, List.of(widgetItem, widgetItem));
|
||||
allEntries.set(3, newTestPackage1ContentEntry);
|
||||
mAdapter.setWidgets(allEntries);
|
||||
|
||||
// THEN the onItemRangeChanged is invoked for "com.google.test1 content" at index 2.
|
||||
verify(mListener).onItemRangeChanged(eq(3), eq(1), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() {
|
||||
// GIVEN a widgets entry list:
|
||||
// Index: 0| 1 | 2| 3 | 4| 5 | 6| 7 | 8| 9 |
|
||||
// [A, A content, B, B content, C, C content, D, D content, E, E content]
|
||||
List<WidgetsListBaseEntry> allAppsWithWidgets = generateSampleMap(5);
|
||||
// GIVEN the current widgets list consist of [A, A content, B, B content, E, E content].
|
||||
// GIVEN the visible widgets list consist of [A, B, E]
|
||||
List<WidgetsListBaseEntry> currentList = List.of(
|
||||
// A & A content
|
||||
allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
|
||||
// B & B content
|
||||
allAppsWithWidgets.get(2), allAppsWithWidgets.get(3),
|
||||
// E & E content
|
||||
allAppsWithWidgets.get(8), allAppsWithWidgets.get(9));
|
||||
mAdapter.setWidgets(currentList);
|
||||
|
||||
// WHEN the widgets list is updated to [A, A content, C, C content, D, D content].
|
||||
// WHEN the visible widgets list is updated to [A, C, D].
|
||||
List<WidgetsListBaseEntry> newList = List.of(
|
||||
// A & A content
|
||||
allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
|
||||
// C & C content
|
||||
allAppsWithWidgets.get(4), allAppsWithWidgets.get(5),
|
||||
// D & D content
|
||||
allAppsWithWidgets.get(6), allAppsWithWidgets.get(7));
|
||||
mAdapter.setWidgets(newList);
|
||||
|
||||
// Account for 1st items as empty space
|
||||
// Computation logic | [Intermediate list during computation]
|
||||
// THEN B <> C < 0, removed B from index 1 | [A, E]
|
||||
verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1);
|
||||
// THEN E <> C > 0, C inserted to index 1 | [A, C, E]
|
||||
verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1);
|
||||
// THEN E <> D > 0, D inserted to index 2 | [A, C, D, E]
|
||||
verify(mListener).onItemRangeInserted(/* positionStart= */ 3, /* itemCount= */ 1);
|
||||
// THEN E <> null = -1, E deleted from index 3 | [A, C, D]
|
||||
verify(mListener).onItemRangeRemoved(/* positionStart= */ 4, /* itemCount= */ 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() {
|
||||
// GIVEN a list of widgets entries:
|
||||
// [Empty item
|
||||
// com.google.test0,
|
||||
// com.google.test0 content,
|
||||
// com.google.test1,
|
||||
// com.google.test1 content,
|
||||
// com.google.test2,
|
||||
// com.google.test2 content]
|
||||
// The visible widgets entries:
|
||||
// [Empty item,
|
||||
// com.google.test0,
|
||||
// com.google.test1,
|
||||
// com.google.test2].
|
||||
ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(3);
|
||||
mAdapter.setWidgetsOnSearch(allEntries);
|
||||
// GIVEN com.google.test.1 header is expanded. The visible entries list becomes:
|
||||
// [Empty item, com.google.test0, com.google.test1, com.google.test1 content,
|
||||
// com.google.test2]
|
||||
mAdapter.onHeaderClicked(/* showWidgets= */ true,
|
||||
new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle));
|
||||
Mockito.reset(mListener);
|
||||
|
||||
// WHEN same widget entries are set again.
|
||||
mAdapter.setWidgetsOnSearch(allEntries);
|
||||
|
||||
// THEN expanded app is reset and the visible entries list becomes:
|
||||
// [Empty item, com.google.test0, com.google.test1, com.google.test2]
|
||||
verify(mListener).onItemRangeChanged(eq(2), eq(1), isNull());
|
||||
verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of sample widget entries.
|
||||
*
|
||||
* <p>Each sample app has 1 widget only. An app is represented by 2 entries,
|
||||
* {@link WidgetsListHeaderEntry} & {@link WidgetsListContentEntry}. Only
|
||||
* {@link WidgetsListHeaderEntry} is always visible in the {@link WidgetsListAdapter}.
|
||||
* {@link WidgetsListContentEntry} is only shown upon clicking the corresponding app's
|
||||
* {@link WidgetsListHeaderEntry}. Only at most one {@link WidgetsListContentEntry} is shown at
|
||||
* a time.
|
||||
*
|
||||
* @param num the number of apps that have widgets.
|
||||
*/
|
||||
private ArrayList<WidgetsListBaseEntry> generateSampleMap(int num) {
|
||||
ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
|
||||
if (num <= 0) return result;
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
String packageName = TEST_PACKAGE_PLACEHOLDER + i;
|
||||
|
||||
List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);
|
||||
|
||||
PackageItemInfo pInfo = new PackageItemInfo(packageName, widgetItems.get(0).user);
|
||||
pInfo.title = pInfo.packageName;
|
||||
pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
|
||||
|
||||
result.add(WidgetsListHeaderEntry.create(
|
||||
pInfo, /* titleSectionName= */ "", widgetItems));
|
||||
result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
|
||||
ArrayList<WidgetItem> widgetItems = new ArrayList<>();
|
||||
for (int i = 0; i < numOfWidgets; i++) {
|
||||
ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
|
||||
AppWidgetProviderInfo widgetInfo = WidgetUtils.createAppWidgetProviderInfo(cn);
|
||||
|
||||
widgetItems.add(new WidgetItem(
|
||||
LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
|
||||
mTestProfile, mIconCache));
|
||||
}
|
||||
return widgetItems;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user