1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
/*
* 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.model.WidgetsListSearchHeaderEntry;
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) {
return !curRow.equals(newRow);
}
if (newRow instanceof WidgetsListSearchHeaderEntry
&& curRow instanceof WidgetsListSearchHeaderEntry) {
// Always refresh search header entries to reset rounded corners in their view holder.
return true;
}
return false;
}
private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
&& !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
}
}
|