1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.widget.picker;
18 
19 import android.util.Log;
20 
21 import androidx.recyclerview.widget.RecyclerView;
22 
23 import com.android.launcher3.icons.IconCache;
24 import com.android.launcher3.model.data.PackageItemInfo;
25 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
26 import com.android.launcher3.widget.model.WidgetsListContentEntry;
27 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
28 import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
29 import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
30 
31 import java.util.ArrayList;
32 import java.util.Iterator;
33 import java.util.List;
34 
35 /**
36  * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
37  * methods accordingly.
38  */
39 public class WidgetsDiffReporter {
40     private static final boolean DEBUG = false;
41     private static final String TAG = "WidgetsDiffReporter";
42 
43     private final IconCache mIconCache;
44     private final RecyclerView.Adapter mListener;
45 
WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener)46     public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) {
47         mIconCache = iconCache;
48         mListener = listener;
49     }
50 
51     /**
52      * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the
53      * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods.
54      */
process(ArrayList<WidgetsListBaseEntry> currentEntries, List<WidgetsListBaseEntry> newEntries, WidgetListBaseRowEntryComparator comparator)55     public void process(ArrayList<WidgetsListBaseEntry> currentEntries,
56             List<WidgetsListBaseEntry> newEntries,
57             WidgetListBaseRowEntryComparator comparator) {
58         if (DEBUG) {
59             Log.d(TAG, "process oldEntries#=" + currentEntries.size()
60                     + " newEntries#=" + newEntries.size());
61         }
62         // Early exit if either of the list is empty
63         if (currentEntries.isEmpty() || newEntries.isEmpty()) {
64             // Skip if both list are empty.
65             // On rotation, we open the widget tray with empty. Then try to fetch the list again
66             // when the animation completes (which still gives empty). And we get the final result
67             // when the bind actually completes.
68             if (currentEntries.size() != newEntries.size()) {
69                 currentEntries.clear();
70                 currentEntries.addAll(newEntries);
71                 mListener.notifyDataSetChanged();
72             }
73             return;
74         }
75         ArrayList<WidgetsListBaseEntry> orgEntries =
76                 (ArrayList<WidgetsListBaseEntry>) currentEntries.clone();
77         Iterator<WidgetsListBaseEntry> orgIter = orgEntries.iterator();
78         Iterator<WidgetsListBaseEntry> newIter = newEntries.iterator();
79 
80         WidgetsListBaseEntry orgRowEntry = orgIter.next();
81         WidgetsListBaseEntry newRowEntry = newIter.next();
82 
83         do {
84             int diff = compareAppNameAndType(orgRowEntry, newRowEntry, comparator);
85             if (DEBUG) {
86                 Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
87                         diff, orgRowEntry != null ? orgRowEntry.toString() : null,
88                         newRowEntry != null ? newRowEntry.toString() : null));
89             }
90             int index = -1;
91             if (diff < 0) {
92                 index = currentEntries.indexOf(orgRowEntry);
93                 mListener.notifyItemRemoved(index);
94                 if (DEBUG) {
95                     Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
96                             orgRowEntry.mTitleSectionName));
97                 }
98                 currentEntries.remove(index);
99                 orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
100             } else if (diff > 0) {
101                 index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry)
102                         : currentEntries.size();
103                 currentEntries.add(index, newRowEntry);
104                 if (DEBUG) {
105                     Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
106                             newRowEntry.mTitleSectionName));
107                 }
108                 newRowEntry = newIter.hasNext() ? newIter.next() : null;
109                 mListener.notifyItemInserted(index);
110 
111             } else {
112                 // same app name & type but,
113                 // did the icon, title, etc, change?
114                 // or did the header view changed due to user interactions?
115                 // or did the widget size and desc, span, etc change?
116                 if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
117                         || hasHeaderUpdated(orgRowEntry, newRowEntry)
118                         || hasWidgetsListContentChanged(orgRowEntry, newRowEntry)) {
119                     index = currentEntries.indexOf(orgRowEntry);
120                     currentEntries.set(index, newRowEntry);
121                     mListener.notifyItemChanged(index);
122                     if (DEBUG) {
123                         Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
124                                 newRowEntry.mTitleSectionName));
125                     }
126                 }
127                 orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
128                 newRowEntry = newIter.hasNext() ? newIter.next() : null;
129             }
130         } while(orgRowEntry != null || newRowEntry != null);
131     }
132 
133     /**
134      * Compares the app name and then entry type for the given {@link WidgetsListBaseEntry}s.
135      *
136      * @Return 0 if both entries' order is the same. Negative integer if {@code newRowEntry} should
137      *         order before {@code orgRowEntry}. Positive integer if {@code orgRowEntry} should
138      *         order before {@code newRowEntry}.
139      */
compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow, WidgetListBaseRowEntryComparator comparator)140     private int compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
141             WidgetListBaseRowEntryComparator comparator) {
142         if (curRow == null && newRow == null) {
143             throw new IllegalStateException(
144                     "Cannot compare PackageItemInfo if both rows are null.");
145         }
146 
147         if (curRow == null && newRow != null) {
148             return 1; // new row needs to be inserted
149         } else if (curRow != null && newRow == null) {
150             return -1; // old row needs to be deleted
151         }
152         int diff = comparator.compare(curRow, newRow);
153         if (diff == 0) {
154             return newRow.getRank() - curRow.getRank();
155         }
156         return diff;
157     }
158 
159     /**
160      * Returns {@code true} if both {@code curRow} & {@code newRow} are
161      * {@link WidgetsListContentEntry}s with a different list or arrangement of widgets.
162      */
hasWidgetsListContentChanged(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow)163     private boolean hasWidgetsListContentChanged(WidgetsListBaseEntry curRow,
164             WidgetsListBaseEntry newRow) {
165         if (!(curRow instanceof WidgetsListContentEntry)
166                 || !(newRow instanceof WidgetsListContentEntry)) {
167             return false;
168         }
169         return !curRow.equals(newRow);
170     }
171 
172     /**
173      * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
174      * been changed due to user interactions.
175      */
hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow)176     private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) {
177         if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) {
178             return !curRow.equals(newRow);
179         }
180         if (newRow instanceof WidgetsListSearchHeaderEntry
181                 && curRow instanceof WidgetsListSearchHeaderEntry) {
182             // Always refresh search header entries to reset rounded corners in their view holder.
183             return true;
184         }
185         return false;
186     }
187 
isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo)188     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
189         return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
190                 && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
191     }
192 }
193