• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 package com.android.launcher3.widget.picker;
17 
18 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_APP_EXPANDED;
19 import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_DEFAULT;
20 import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_FIRST;
21 import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_LAST;
22 import static com.android.launcher3.widget.BaseWidgetSheet.DEFAULT_MAX_HORIZONTAL_SPANS;
23 
24 import android.content.Context;
25 import android.os.Process;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.View.OnClickListener;
31 import android.view.View.OnLongClickListener;
32 import android.view.ViewGroup;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.Px;
37 import androidx.recyclerview.widget.DiffUtil;
38 import androidx.recyclerview.widget.DiffUtil.DiffResult;
39 import androidx.recyclerview.widget.LinearLayoutManager;
40 import androidx.recyclerview.widget.RecyclerView;
41 import androidx.recyclerview.widget.RecyclerView.Adapter;
42 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
43 
44 import com.android.launcher3.R;
45 import com.android.launcher3.model.data.PackageItemInfo;
46 import com.android.launcher3.recyclerview.ViewHolderBinder;
47 import com.android.launcher3.util.LabelComparator;
48 import com.android.launcher3.util.PackageUserKey;
49 import com.android.launcher3.views.ActivityContext;
50 import com.android.launcher3.widget.model.WidgetListSpaceEntry;
51 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
52 import com.android.launcher3.widget.model.WidgetsListContentEntry;
53 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
54 import com.android.launcher3.widget.util.WidgetSizes;
55 
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collections;
59 import java.util.Comparator;
60 import java.util.List;
61 import java.util.Map;
62 import java.util.OptionalInt;
63 import java.util.function.IntSupplier;
64 import java.util.function.Predicate;
65 import java.util.stream.Collectors;
66 import java.util.stream.IntStream;
67 
68 /**
69  * Recycler view adapter for the widget tray.
70  *
71  * <p>This adapter supports view binding of subclasses of {@link WidgetsListBaseEntry}. There are 2
72  * subclasses: {@link WidgetsListHeader} & {@link WidgetsListContentEntry}.
73  * {@link WidgetsListHeader} entries are always visible in the recycler view. At most one
74  * {@link WidgetsListContentEntry} is shown in the recycler view at any time. Clicking a
75  * {@link WidgetsListHeader} will result in expanding / collapsing a corresponding
76  * {@link WidgetsListContentEntry} of the same app.
77  */
78 public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderClickListener {
79 
80     private static final String TAG = "WidgetsListAdapter";
81     private static final boolean DEBUG = false;
82 
83     /** Uniquely identifies widgets list view type within the app. */
84     public static final int VIEW_TYPE_WIDGETS_SPACE = R.id.view_type_widgets_space;
85     public static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
86     public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
87 
88     private final Context mContext;
89     private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
90     private final WidgetListBaseRowEntryComparator mRowComparator =
91             new WidgetListBaseRowEntryComparator();
92     @Nullable private final WidgetsFullSheet.HeaderChangeListener mHeaderChangeListener;
93 
94     private final List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
95     private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
96     @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;
97 
98     private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
99             entry instanceof WidgetsListHeaderEntry
100                     || PackageUserKey.fromPackageItemInfo(entry.mPkgItem)
101                             .equals(mWidgetsContentVisiblePackageUserKey);
102     @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
103     @Nullable private RecyclerView mRecyclerView;
104     @Nullable private PackageUserKey mPendingClickHeader;
105     @Px private int mMaxHorizontalSpan;
106 
WidgetsListAdapter(Context context, LayoutInflater layoutInflater, IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener, OnLongClickListener iconLongClickListener, WidgetsFullSheet.HeaderChangeListener headerChangeListener)107     public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
108             IntSupplier emptySpaceHeightProvider, OnClickListener iconClickListener,
109             OnLongClickListener iconLongClickListener,
110             WidgetsFullSheet.HeaderChangeListener headerChangeListener) {
111         mHeaderChangeListener = headerChangeListener;
112         mContext = context;
113         mMaxHorizontalSpan = WidgetSizes.getWidgetSizePx(
114                 ActivityContext.lookupContext(context).getDeviceProfile(),
115                         DEFAULT_MAX_HORIZONTAL_SPANS, 1).getWidth();
116 
117         mViewHolderBinders.put(
118                 VIEW_TYPE_WIDGETS_LIST,
119                 new WidgetsListTableViewHolderBinder(
120                         mContext, layoutInflater, iconClickListener, iconLongClickListener));
121         mViewHolderBinders.put(
122                 VIEW_TYPE_WIDGETS_HEADER,
123                 new WidgetsListHeaderViewHolderBinder(
124                         layoutInflater, /* onHeaderClickListener= */ this,
125                         headerChangeListener != null));
126         mViewHolderBinders.put(
127                 VIEW_TYPE_WIDGETS_SPACE,
128                 new WidgetsSpaceViewHolderBinder(emptySpaceHeightProvider));
129     }
130 
131     @Override
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)132     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
133         mRecyclerView = recyclerView;
134     }
135 
136     @Override
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)137     public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
138         mRecyclerView = null;
139     }
140 
setFilter(Predicate<WidgetsListBaseEntry> filter)141     public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
142         mFilter = filter;
143     }
144 
145     @Override
getItemCount()146     public int getItemCount() {
147         return mVisibleEntries.size();
148     }
149 
150     /**
151      * Returns true if the adapter has entries which will be visible to the user
152      */
hasVisibleEntries()153     public boolean hasVisibleEntries() {
154         // Account for the 1st space entry
155         return getItemCount() > 1;
156     }
157 
158     /** Returns all items that will be drawn in a recycler view. */
getItems()159     public List<WidgetsListBaseEntry> getItems() {
160         return mVisibleEntries;
161     }
162 
163     /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
getSectionName(int pos)164     public String getSectionName(int pos) {
165         return mVisibleEntries.get(pos).mTitleSectionName;
166     }
167 
168     /** Updates the widget list based on {@code tempEntries}. */
setWidgets(List<WidgetsListBaseEntry> tempEntries)169     public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
170         mAllEntries.clear();
171         mAllEntries.add(new WidgetListSpaceEntry());
172         tempEntries.stream().sorted(mRowComparator).forEach(mAllEntries::add);
173         if (shouldClearVisibleEntries()) {
174             mVisibleEntries.clear();
175         }
176         updateVisibleEntries();
177     }
178 
179     /** Updates the widget list based on {@code searchResults}. */
setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults)180     public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
181         // Forget the expanded package every time widget list is refreshed in search mode.
182         mWidgetsContentVisiblePackageUserKey = null;
183         setWidgets(searchResults);
184     }
185 
updateVisibleEntries()186     private void updateVisibleEntries() {
187         // Get the current top of the header with the matching key before adjusting the visible
188         // entries.
189         OptionalInt previousPositionForPackageUserKey =
190                 getPositionForPackageUserKey(mPendingClickHeader);
191         OptionalInt topForPackageUserKey =
192                 getOffsetForPosition(previousPositionForPackageUserKey);
193 
194         List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
195                 .filter(entry -> (((mFilter == null || mFilter.test(entry))
196                         && mHeaderAndSelectedContentFilter.test(entry))
197                         || entry instanceof WidgetListSpaceEntry)
198                         && (mHeaderChangeListener == null
199                         || !(entry instanceof WidgetsListContentEntry)))
200                 .map(entry -> {
201                     if (entry instanceof WidgetsListHeaderEntry
202                             && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
203                         // Adjust the original entries to expand headers for the selected content.
204                         return ((WidgetsListHeaderEntry) entry).withWidgetListShown();
205                     } else if (entry instanceof WidgetsListContentEntry) {
206                         // Adjust the original content entries to accommodate for the current
207                         // maxSpanSize.
208                         return ((WidgetsListContentEntry) entry).withMaxSpanSize(
209                                 mMaxHorizontalSpan);
210                     }
211                     return entry;
212                 })
213                 .collect(Collectors.toList());
214 
215         DiffResult diffResult = DiffUtil.calculateDiff(
216                 new WidgetsDiffCallback(mVisibleEntries, newVisibleEntries), false);
217         mVisibleEntries.clear();
218         mVisibleEntries.addAll(newVisibleEntries);
219         diffResult.dispatchUpdatesTo(this);
220 
221         if (mPendingClickHeader != null) {
222             // Get the position for the clicked header after adjusting the visible entries. The
223             // position may have changed if another header had previously been expanded.
224             OptionalInt positionForPackageUserKey =
225                     getPositionForPackageUserKey(mPendingClickHeader);
226             scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
227             mPendingClickHeader = null;
228         }
229     }
230 
231     /** Returns whether {@code entry} matches {@code key}. */
isHeaderForPackageUserKey( @onNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key)232     private static boolean isHeaderForPackageUserKey(
233             @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
234         return entry instanceof WidgetsListHeaderEntry && matchesKey(entry, key);
235     }
236 
matchesKey(@onNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key)237     private static boolean matchesKey(@NonNull WidgetsListBaseEntry entry,
238             @Nullable PackageUserKey key) {
239         if (key == null) return false;
240         return entry.mPkgItem.packageName.equals(key.mPackageName)
241                 && entry.mPkgItem.widgetCategory == key.mWidgetCategory
242                 && entry.mPkgItem.user.equals(key.mUser);
243     }
244 
245     /**
246      * Resets any expanded widget header.
247      */
resetExpandedHeader()248     public void resetExpandedHeader() {
249         if (mWidgetsContentVisiblePackageUserKey != null) {
250             mWidgetsContentVisiblePackageUserKey = null;
251             updateVisibleEntries();
252         }
253     }
254 
255     @Override
onBindViewHolder(ViewHolder holder, int position)256     public void onBindViewHolder(ViewHolder holder, int position) {
257         onBindViewHolder(holder, position, Collections.EMPTY_LIST);
258     }
259 
260     @Override
onBindViewHolder(ViewHolder holder, int pos, List<Object> payloads)261     public void onBindViewHolder(ViewHolder holder, int pos, List<Object> payloads) {
262         ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
263 
264         // The first entry has an empty space, count from second entries.
265         int listPos = (pos > 1) ? POSITION_DEFAULT : POSITION_FIRST;
266         if (pos == (getItemCount() - 1)) {
267             listPos |= POSITION_LAST;
268         }
269         viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
270     }
271 
272     /**
273      * Selects the first visible header. This is used in search as we want to always select the
274      * first header in the new list that gets generated as we search.
275      */
selectFirstHeaderEntry()276     void selectFirstHeaderEntry() {
277         mVisibleEntries.stream()
278                 .filter(entry -> entry instanceof WidgetsListHeaderEntry)
279                 .findFirst()
280                 .ifPresent(entry ->
281                         onHeaderClicked(true, PackageUserKey.fromPackageItemInfo(entry.mPkgItem)));
282     }
283 
284     @Override
onCreateViewHolder(ViewGroup parent, int viewType)285     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
286         if (DEBUG) {
287             Log.v(TAG, "\nonCreateViewHolder");
288         }
289 
290         return mViewHolderBinders.get(viewType).newViewHolder(parent);
291     }
292 
293     @Override
onViewRecycled(ViewHolder holder)294     public void onViewRecycled(ViewHolder holder) {
295         mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder);
296     }
297 
298     @Override
onFailedToRecycleView(ViewHolder holder)299     public boolean onFailedToRecycleView(ViewHolder holder) {
300         // If child views are animating, then the RecyclerView may choose not to recycle the view,
301         // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
302         // recycling this view, and take care in onViewRecycled() to cancel any existing
303         // animations.
304         return true;
305     }
306 
307     @Override
getItemId(int pos)308     public long getItemId(int pos) {
309         return Arrays.hashCode(new Object[]{
310                 mVisibleEntries.get(pos).mPkgItem.hashCode(),
311                 getItemViewType(pos)});
312     }
313 
314     @Override
getItemViewType(int pos)315     public int getItemViewType(int pos) {
316         WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
317         if (entry instanceof WidgetsListContentEntry) {
318             return VIEW_TYPE_WIDGETS_LIST;
319         } else if (entry instanceof WidgetsListHeaderEntry) {
320             return VIEW_TYPE_WIDGETS_HEADER;
321         } else if (entry instanceof WidgetListSpaceEntry) {
322             return VIEW_TYPE_WIDGETS_SPACE;
323         }
324         throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
325     }
326 
327     @Override
onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey)328     public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) {
329         // Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
330         if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
331 
332         if (mHeaderChangeListener != null
333                 && packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
334 
335         if (showWidgets) {
336             mWidgetsContentVisiblePackageUserKey = packageUserKey;
337             ActivityContext.lookupContext(mContext)
338                     .getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED);
339         } else {
340             mWidgetsContentVisiblePackageUserKey = null;
341         }
342 
343         // Store the header that was clicked so that its position will be maintained the next time
344         // we update the entries.
345         mPendingClickHeader = packageUserKey;
346 
347         updateVisibleEntries();
348 
349         if (mHeaderChangeListener != null && mWidgetsContentVisiblePackageUserKey != null) {
350             mHeaderChangeListener.onHeaderChanged(mWidgetsContentVisiblePackageUserKey);
351         }
352     }
353 
354     /**
355      * Returns the position of {@code key} in {@link #mVisibleEntries}, or  empty if it's not
356      * present.
357      */
358     @NonNull
getPositionForPackageUserKey(@ullable PackageUserKey key)359     private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) {
360         return IntStream.range(0, mVisibleEntries.size())
361                 .filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key))
362                 .findFirst();
363     }
364 
365     /**
366      * Returns the top of {@code positionOptional} in the recycler view, or empty if its view
367      * can't be found for any reason, including the position not being currently visible. The
368      * returned value does not include the top padding of the recycler view.
369      */
getOffsetForPosition(OptionalInt positionOptional)370     private OptionalInt getOffsetForPosition(OptionalInt positionOptional) {
371         if (!positionOptional.isPresent() || mRecyclerView == null) return OptionalInt.empty();
372 
373         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
374         if (layoutManager == null) return OptionalInt.empty();
375 
376         View view = layoutManager.findViewByPosition(positionOptional.getAsInt());
377         if (view == null) return OptionalInt.empty();
378 
379         return OptionalInt.of(layoutManager.getDecoratedTop(view));
380     }
381 
382     /**
383      * Scrolls to the selected header position with the provided offset. LinearLayoutManager
384      * scrolls the minimum distance necessary, so this will keep the selected header in place during
385      * clicks, without interrupting the animation.
386      *
387      * @param positionOptional The position too scroll to. No scrolling will be done if empty.
388      * @param offsetOptional The offset from the top to maintain. If empty, then the list will
389      *                       scroll to the top of the position.
390      */
scrollToPositionAndMaintainOffset( OptionalInt positionOptional, OptionalInt offsetOptional)391     private void scrollToPositionAndMaintainOffset(
392             OptionalInt positionOptional,
393             OptionalInt offsetOptional) {
394         if (!positionOptional.isPresent() || mRecyclerView == null) return;
395         int position = positionOptional.getAsInt();
396 
397         LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
398         if (layoutManager == null) return;
399 
400         if (position == mVisibleEntries.size() - 2
401                 && mVisibleEntries.get(mVisibleEntries.size() - 1)
402                 instanceof WidgetsListContentEntry) {
403             // If the selected header is in the last position and its content is showing, then
404             // scroll to the final position so the last list of widgets will show.
405             layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
406             return;
407         }
408 
409         // Scroll to the header view's current offset, accounting for the recycler view's padding.
410         // If the header view couldn't be found, then it will appear at the top of the list.
411         layoutManager.scrollToPositionWithOffset(
412                 position,
413                 offsetOptional.orElse(0) - mRecyclerView.getPaddingTop());
414     }
415 
416     /**
417      * Sets the max horizontal span in pixels that is allowed for grouping more than one widget in a
418      * table row.
419      */
setMaxHorizontalSpansPxPerRow(@x int maxHorizontalSpan)420     public void setMaxHorizontalSpansPxPerRow(@Px int maxHorizontalSpan) {
421         mMaxHorizontalSpan = maxHorizontalSpan;
422         updateVisibleEntries();
423     }
424 
425     /**
426      * Returns {@code true} if there is a change in {@link #mAllEntries} that results in an
427      * invalidation of {@link #mVisibleEntries}. e.g. there is change in the device language.
428      */
shouldClearVisibleEntries()429     private boolean shouldClearVisibleEntries() {
430         Map<PackageUserKey, PackageItemInfo> packagesInfo =
431                 mAllEntries.stream()
432                         .filter(entry -> entry instanceof WidgetsListHeaderEntry)
433                         .map(entry -> entry.mPkgItem)
434                         .collect(Collectors.toMap(
435                                 entry -> PackageUserKey.fromPackageItemInfo(entry),
436                                 entry -> entry));
437         for (WidgetsListBaseEntry visibleEntry: mVisibleEntries) {
438             PackageUserKey key = PackageUserKey.fromPackageItemInfo(visibleEntry.mPkgItem);
439             PackageItemInfo packageItemInfo = packagesInfo.get(key);
440             if (packageItemInfo != null
441                     && !visibleEntry.mPkgItem.title.equals(packageItemInfo.title)) {
442                 return true;
443             }
444         }
445         return false;
446     }
447 
448     /** Comparator for sorting WidgetListRowEntry based on package title. */
449     public static class WidgetListBaseRowEntryComparator implements
450             Comparator<WidgetsListBaseEntry> {
451 
452         private final LabelComparator mComparator = new LabelComparator();
453 
454         @Override
compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b)455         public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) {
456             int i = mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString());
457             if (i != 0) {
458                 return i;
459             }
460             // Prioritize entries from current user over other users if the entries are same.
461             if (a.mPkgItem.user.equals(b.mPkgItem.user)) return 0;
462             if (a.mPkgItem.user.equals(Process.myUserHandle())) return -1;
463             return 1;
464         }
465     }
466 }
467