Merge "Using DiffUtil for calculating widget diff instead of a custom implementation" into tm-qpr-dev am: cc10ed5532 am: 75db2c1f59 am: 3425e350ac
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/21381953 Change-Id: Ia264a9a0b0d7b08b8bd59e3b3fbd179ebbf3b6c8 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);
|
Collections.EMPTY_LIST);
|
||||||
mPkgItem.title = "";
|
mPkgItem.title = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getRank() {
|
|
||||||
return RANK_WIDGETS_TOP_SPACE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,11 @@
|
|||||||
|
|
||||||
package com.android.launcher3.widget.model;
|
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.WidgetItem;
|
||||||
import com.android.launcher3.model.data.ItemInfo;
|
import com.android.launcher3.model.data.ItemInfo;
|
||||||
import com.android.launcher3.model.data.PackageItemInfo;
|
import com.android.launcher3.model.data.PackageItemInfo;
|
||||||
import com.android.launcher3.widget.WidgetItemComparator;
|
import com.android.launcher3.widget.WidgetItemComparator;
|
||||||
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -48,23 +43,4 @@ public abstract class WidgetsListBaseEntry {
|
|||||||
this.mWidgets =
|
this.mWidgets =
|
||||||
items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
|
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;
|
+ mMaxSpanSizeInCells;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Rank
|
|
||||||
public int getRank() {
|
|
||||||
return RANK_WIDGETS_LIST_CONTENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of this {@link WidgetsListContentEntry} with updated
|
* Returns a copy of this {@link WidgetsListContentEntry} with updated
|
||||||
* {@param maxSpanSizeInCells}.
|
* {@param maxSpanSizeInCells}.
|
||||||
|
|||||||
@@ -85,12 +85,6 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
|
|||||||
return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
|
return "Header:" + mPkgItem.packageName + ":" + mWidgets.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Rank
|
|
||||||
public int getRank() {
|
|
||||||
return RANK_WIDGETS_LIST_HEADER;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSearchEntry() {
|
public boolean isSearchEntry() {
|
||||||
return mIsSearchEntry;
|
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.DeviceProfile;
|
||||||
import com.android.launcher3.Launcher;
|
import com.android.launcher3.Launcher;
|
||||||
import com.android.launcher3.LauncherAppState;
|
|
||||||
import com.android.launcher3.R;
|
import com.android.launcher3.R;
|
||||||
import com.android.launcher3.Utilities;
|
import com.android.launcher3.Utilities;
|
||||||
import com.android.launcher3.anim.PendingAnimation;
|
import com.android.launcher3.anim.PendingAnimation;
|
||||||
@@ -961,8 +960,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
|||||||
AdapterHolder(int adapterType) {
|
AdapterHolder(int adapterType) {
|
||||||
mAdapterType = adapterType;
|
mAdapterType = adapterType;
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
LauncherAppState apps = LauncherAppState.getInstance(context);
|
|
||||||
|
|
||||||
HeaderChangeListener headerChangeListener = new HeaderChangeListener() {
|
HeaderChangeListener headerChangeListener = new HeaderChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
|
public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
|
||||||
@@ -993,7 +990,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
|
|||||||
mWidgetsListAdapter = new WidgetsListAdapter(
|
mWidgetsListAdapter = new WidgetsListAdapter(
|
||||||
context,
|
context,
|
||||||
LayoutInflater.from(context),
|
LayoutInflater.from(context),
|
||||||
apps.getIconCache(),
|
|
||||||
this::getEmptySpaceHeight,
|
this::getEmptySpaceHeight,
|
||||||
/* iconClickListener= */ WidgetsFullSheet.this,
|
/* iconClickListener= */ WidgetsFullSheet.this,
|
||||||
/* iconLongClickListener= */ WidgetsFullSheet.this,
|
/* iconLongClickListener= */ WidgetsFullSheet.this,
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil.DiffResult;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
|
|
||||||
import com.android.launcher3.R;
|
import com.android.launcher3.R;
|
||||||
import com.android.launcher3.icons.IconCache;
|
|
||||||
import com.android.launcher3.model.data.PackageItemInfo;
|
import com.android.launcher3.model.data.PackageItemInfo;
|
||||||
import com.android.launcher3.recyclerview.ViewHolderBinder;
|
import com.android.launcher3.recyclerview.ViewHolderBinder;
|
||||||
import com.android.launcher3.util.LabelComparator;
|
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;
|
public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final WidgetsDiffReporter mDiffReporter;
|
|
||||||
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
|
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
|
||||||
private final WidgetListBaseRowEntryComparator mRowComparator =
|
private final WidgetListBaseRowEntryComparator mRowComparator =
|
||||||
new WidgetListBaseRowEntryComparator();
|
new WidgetListBaseRowEntryComparator();
|
||||||
@@ -102,12 +102,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||||||
private int mMaxSpanSize = 4;
|
private int mMaxSpanSize = 4;
|
||||||
|
|
||||||
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
|
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
|
||||||
IconCache iconCache, IntSupplier emptySpaceHeightProvider,
|
IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener,
|
||||||
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,
|
OnLongClickListener iconLongClickListener,
|
||||||
WidgetsFullSheet.HeaderChangeListener headerChangeListener) {
|
WidgetsFullSheet.HeaderChangeListener headerChangeListener) {
|
||||||
mHeaderChangeListener = headerChangeListener;
|
mHeaderChangeListener = headerChangeListener;
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
|
|
||||||
|
|
||||||
mViewHolderBinders.put(
|
mViewHolderBinders.put(
|
||||||
VIEW_TYPE_WIDGETS_LIST,
|
VIEW_TYPE_WIDGETS_LIST,
|
||||||
@@ -205,7 +204,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.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) {
|
if (mPendingClickHeader != null) {
|
||||||
// Get the position for the clicked header after adjusting the visible entries. The
|
// 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.WidgetsListBaseEntry;
|
||||||
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
import com.android.launcher3.widget.model.WidgetsListContentEntry;
|
||||||
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
|
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
|
||||||
import com.android.launcher3.widget.picker.WidgetsDiffReporter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -73,8 +72,7 @@ public class WidgetsModel {
|
|||||||
/**
|
/**
|
||||||
* Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
|
* 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
|
* 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 WidgetsListBaseEntry}s is not sorted.
|
||||||
* {@link WidgetsDiffReporter}
|
|
||||||
*
|
*
|
||||||
* @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
|
* @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