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