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