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.allapps; 17 18 import static android.multiuser.Flags.enableMovingContentIntoPrivateSpace; 19 20 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO; 21 import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER; 22 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT; 23 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT; 24 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; 25 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE; 26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT; 27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT; 28 29 import android.content.Context; 30 import android.text.Spannable; 31 import android.text.SpannableString; 32 import android.text.style.ImageSpan; 33 import android.util.Log; 34 35 import androidx.annotation.Nullable; 36 import androidx.annotation.VisibleForTesting; 37 import androidx.recyclerview.widget.DiffUtil; 38 39 import com.android.launcher3.Flags; 40 import com.android.launcher3.R; 41 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; 42 import com.android.launcher3.model.data.AppInfo; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.util.LabelComparator; 45 import com.android.launcher3.views.ActivityContext; 46 47 import java.io.PrintWriter; 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Locale; 51 import java.util.Map; 52 import java.util.Objects; 53 import java.util.TreeMap; 54 import java.util.function.Predicate; 55 import java.util.stream.Collectors; 56 import java.util.stream.Stream; 57 58 /** 59 * The alphabetically sorted list of applications. 60 * 61 * @param <T> Type of context inflating this view. 62 */ 63 public class AlphabeticalAppsList<T extends Context & ActivityContext> implements 64 AllAppsStore.OnUpdateListener { 65 66 public static final String TAG = "AlphabeticalAppsList"; 67 public static final String PRIVATE_SPACE_PACKAGE = "com.android.privatespace"; 68 69 private final WorkProfileManager mWorkProviderManager; 70 71 private final PrivateProfileManager mPrivateProviderManager; 72 73 /** 74 * Info about a fast scroller section, depending if sections are merged, the fast scroller 75 * sections will not be the same set as the section headers. 76 */ 77 public static class FastScrollSectionInfo { 78 // The section name 79 public final CharSequence sectionName; 80 // The item position 81 public final int position; 82 // The view id associated with this section 83 public int id = -1; 84 FastScrollSectionInfo(CharSequence sectionName, int position)85 public FastScrollSectionInfo(CharSequence sectionName, int position) { 86 this.sectionName = sectionName; 87 this.position = position; 88 } 89 setId(int id)90 public void setId(int id) { 91 this.id = id; 92 } 93 } 94 95 96 private final T mActivityContext; 97 98 // The set of apps from the system 99 private final List<AppInfo> mApps = new ArrayList<>(); 100 private final List<AppInfo> mPrivateApps = new ArrayList<>(); 101 @Nullable 102 private final AllAppsStore<T> mAllAppsStore; 103 104 // The number of results in current adapter 105 private int mAccessibilityResultsCount = 0; 106 // The current set of adapter items 107 private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>(); 108 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 109 private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 110 111 // The of ordered component names as a result of a search query 112 private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>(); 113 private final SpannableString mPrivateProfileAppScrollerBadge; 114 private final SpannableString mPrivateProfileDividerBadge; 115 private BaseAllAppsAdapter<T> mAdapter; 116 private AppInfoComparator mAppNameComparator; 117 private int mNumAppsPerRowAllApps; 118 private int mNumAppRowsInAdapter; 119 private Predicate<ItemInfo> mItemFilter; 120 AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore, WorkProfileManager workProfileManager, PrivateProfileManager privateProfileManager)121 public AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore, 122 WorkProfileManager workProfileManager, PrivateProfileManager privateProfileManager) { 123 mAllAppsStore = appsStore; 124 mActivityContext = ActivityContext.lookupContext(context); 125 mAppNameComparator = new AppInfoComparator(context); 126 mWorkProviderManager = workProfileManager; 127 mPrivateProviderManager = privateProfileManager; 128 mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().numShownAllAppsColumns; 129 if (mAllAppsStore != null) { 130 mAllAppsStore.addUpdateListener(this); 131 } 132 mPrivateProfileAppScrollerBadge = new SpannableString(" "); 133 mPrivateProfileAppScrollerBadge.setSpan(new ImageSpan(context, Flags.letterFastScroller() 134 ? R.drawable.ic_private_profile_letter_list_fast_scroller_badge : 135 R.drawable.ic_private_profile_app_scroller_badge, ImageSpan.ALIGN_CENTER), 136 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 137 mPrivateProfileDividerBadge = new SpannableString(" "); 138 mPrivateProfileDividerBadge.setSpan(new ImageSpan(context, 139 R.drawable.ic_private_profile_divider_badge, ImageSpan.ALIGN_CENTER), 140 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 141 } 142 143 /** Set the number of apps per row when device profile changes. */ setNumAppsPerRowAllApps(int numAppsPerRow)144 public void setNumAppsPerRowAllApps(int numAppsPerRow) { 145 mNumAppsPerRowAllApps = numAppsPerRow; 146 } 147 updateItemFilter(Predicate<ItemInfo> itemFilter)148 public void updateItemFilter(Predicate<ItemInfo> itemFilter) { 149 this.mItemFilter = itemFilter; 150 onAppsUpdated(); 151 } 152 153 /** 154 * Sets the adapter to notify when this dataset changes. 155 */ setAdapter(BaseAllAppsAdapter<T> adapter)156 public void setAdapter(BaseAllAppsAdapter<T> adapter) { 157 mAdapter = adapter; 158 } 159 160 /** 161 * Returns fast scroller sections of all the current filtered applications. 162 */ getFastScrollerSections()163 public List<FastScrollSectionInfo> getFastScrollerSections() { 164 return mFastScrollerSections; 165 } 166 167 /** 168 * Returns the current filtered list of applications broken down into their sections. 169 */ getAdapterItems()170 public List<AdapterItem> getAdapterItems() { 171 return mAdapterItems; 172 } 173 174 /** 175 * Returns the child adapter item with IME launch focus. 176 */ getFocusedChild()177 public AdapterItem getFocusedChild() { 178 if (mAdapterItems.size() == 0 || getFocusedChildIndex() == -1) { 179 return null; 180 } 181 return mAdapterItems.get(getFocusedChildIndex()); 182 } 183 184 /** 185 * Returns the index of the child with IME launch focus. 186 */ getFocusedChildIndex()187 public int getFocusedChildIndex() { 188 for (AdapterItem item : mAdapterItems) { 189 if (item.isCountedForAccessibility()) { 190 return mAdapterItems.indexOf(item); 191 } 192 } 193 return -1; 194 } 195 196 /** 197 * Returns the number of rows of applications 198 */ getNumAppRows()199 public int getNumAppRows() { 200 return mNumAppRowsInAdapter; 201 } 202 203 /** 204 * Returns the number of applications in this list. 205 */ getNumFilteredApps()206 public int getNumFilteredApps() { 207 return mAccessibilityResultsCount; 208 } 209 210 /** 211 * Returns whether there are search results which will hide the A-Z list. 212 */ hasSearchResults()213 public boolean hasSearchResults() { 214 return !mSearchResults.isEmpty(); 215 } 216 217 /** 218 * Sets results list for search 219 */ setSearchResults(ArrayList<AdapterItem> results)220 public boolean setSearchResults(ArrayList<AdapterItem> results) { 221 if (Objects.equals(results, mSearchResults)) { 222 return false; 223 } 224 mSearchResults.clear(); 225 if (results != null) { 226 mSearchResults.addAll(results); 227 } 228 updateAdapterItems(); 229 return true; 230 } 231 232 /** 233 * Updates internals when the set of apps are updated. 234 */ 235 @Override onAppsUpdated()236 public void onAppsUpdated() { 237 // Don't update apps when the private profile animations are running, otherwise the motion 238 // is canceled. 239 if (mAllAppsStore == null || (mPrivateProviderManager != null && 240 mPrivateProviderManager.getAnimationRunning())) { 241 return; 242 } 243 // Sort the list of apps 244 mApps.clear(); 245 mPrivateApps.clear(); 246 247 Stream<AppInfo> appSteam = Stream.of(mAllAppsStore.getApps()); 248 Stream<AppInfo> privateAppStream = Stream.of(mAllAppsStore.getApps()); 249 250 if (!hasSearchResults() && mItemFilter != null) { 251 appSteam = appSteam.filter(mItemFilter); 252 if (mPrivateProviderManager != null) { 253 privateAppStream = privateAppStream 254 .filter(mPrivateProviderManager.getItemInfoMatcher()); 255 } 256 } 257 appSteam = appSteam.sorted(mAppNameComparator); 258 privateAppStream = privateAppStream.sorted(mAppNameComparator); 259 260 // As a special case for some languages (currently only Simplified Chinese), we may need to 261 // coalesce sections 262 Locale curLocale = mActivityContext.getResources().getConfiguration().locale; 263 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 264 if (localeRequiresSectionSorting) { 265 // Compute the section headers. We use a TreeMap with the section name comparator to 266 // ensure that the sections are ordered when we iterate over it later 267 appSteam = appSteam.collect(Collectors.groupingBy( 268 info -> info.sectionName, 269 () -> new TreeMap<>(new LabelComparator()), 270 Collectors.toCollection(ArrayList::new))) 271 .values() 272 .stream() 273 .flatMap(ArrayList::stream); 274 } 275 276 appSteam.forEachOrdered(mApps::add); 277 privateAppStream.forEachOrdered(mPrivateApps::add); 278 // Recompose the set of adapter items from the current set of apps 279 if (mSearchResults.isEmpty()) { 280 updateAdapterItems(); 281 } 282 } 283 284 /** 285 * Updates the set of filtered apps with the current filter. At this point, we expect 286 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 287 */ updateAdapterItems()288 public void updateAdapterItems() { 289 List<AdapterItem> oldItems = new ArrayList<>(mAdapterItems); 290 // Prepare to update the list of sections, filtered apps, etc. 291 mFastScrollerSections.clear(); 292 Log.d(TAG, "Clearing FastScrollerSections."); 293 mAdapterItems.clear(); 294 mAccessibilityResultsCount = 0; 295 296 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 297 // ordered set of sections 298 if (hasSearchResults()) { 299 mAdapterItems.addAll(mSearchResults); 300 } else { 301 int position = 0; 302 boolean addApps = true; 303 if (mWorkProviderManager != null) { 304 position += mWorkProviderManager.addWorkItems(mAdapterItems); 305 addApps = mWorkProviderManager.shouldShowWorkApps(); 306 } 307 if (addApps) { 308 if (/* education card was added */ position == 1) { 309 // Add work educard section with "info icon" at 0th position. 310 mFastScrollerSections.add(new FastScrollSectionInfo( 311 mActivityContext.getResources().getString( 312 R.string.work_profile_edu_section), 0)); 313 Log.d(TAG, "Adding FastScrollSection for work edu card."); 314 } 315 position = addAppsWithSections(mApps, position); 316 } 317 if (Flags.enablePrivateSpace()) { 318 position = addPrivateSpaceItems(position); 319 } 320 if (!mFastScrollerSections.isEmpty()) { 321 // After all the adapterItems are added, add a view to the bottom so that user can 322 // scroll all the way down. 323 mAdapterItems.add(new AdapterItem(VIEW_TYPE_BOTTOM_VIEW_TO_SCROLL_TO)); 324 mFastScrollerSections.add(new FastScrollSectionInfo( 325 mFastScrollerSections.get(mFastScrollerSections.size() - 1).sectionName, 326 position++)); 327 Log.d(TAG, "Adding FastScrollSection duplicate to scroll to the bottom."); 328 } 329 } 330 mAccessibilityResultsCount = (int) mAdapterItems.stream() 331 .filter(AdapterItem::isCountedForAccessibility).count(); 332 333 if (mNumAppsPerRowAllApps != 0) { 334 // Update the number of rows in the adapter after we do all the merging (otherwise, we 335 // would have to shift the values again) 336 int numAppsInSection = 0; 337 int numAppsInRow = 0; 338 int rowIndex = -1; 339 for (AdapterItem item : mAdapterItems) { 340 item.rowIndex = 0; 341 if (BaseAllAppsAdapter.isDividerViewType(item.viewType) 342 || BaseAllAppsAdapter.isPrivateSpaceHeaderView(item.viewType) 343 || BaseAllAppsAdapter.isPrivateSpaceSysAppsDividerView(item.viewType)) { 344 numAppsInSection = 0; 345 } else if (BaseAllAppsAdapter.isIconViewType(item.viewType)) { 346 if (numAppsInSection % mNumAppsPerRowAllApps == 0) { 347 numAppsInRow = 0; 348 rowIndex++; 349 } 350 item.rowIndex = rowIndex; 351 item.rowAppIndex = numAppsInRow; 352 numAppsInSection++; 353 numAppsInRow++; 354 } 355 } 356 mNumAppRowsInAdapter = rowIndex + 1; 357 } 358 359 if (mAdapter != null) { 360 DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false) 361 .dispatchUpdatesTo(mAdapter); 362 } 363 } 364 addPrivateSpaceItems(int position)365 int addPrivateSpaceItems(int position) { 366 if (mPrivateProviderManager != null 367 && !mPrivateProviderManager.isPrivateSpaceHidden() 368 && !mPrivateApps.isEmpty()) { 369 // Always add PS Header if Space is present and visible. 370 position = mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems); 371 Log.d(TAG, "Adding FastScrollSection for Private Space header. "); 372 mFastScrollerSections.add(new FastScrollSectionInfo( 373 mPrivateProfileAppScrollerBadge, position)); 374 int privateSpaceState = mPrivateProviderManager.getCurrentState(); 375 switch (privateSpaceState) { 376 case PrivateProfileManager.STATE_DISABLED: 377 case PrivateProfileManager.STATE_TRANSITION: 378 break; 379 case PrivateProfileManager.STATE_ENABLED: 380 // Add PS Apps only in Enabled State. 381 position = addPrivateSpaceApps(position); 382 break; 383 } 384 } 385 return position; 386 } 387 addPrivateSpaceApps(int position)388 private int addPrivateSpaceApps(int position) { 389 // Add Install Apps Button first. 390 if (Flags.privateSpaceAppInstallerButton() && !enableMovingContentIntoPrivateSpace()) { 391 mPrivateProviderManager.addPrivateSpaceInstallAppButton(mAdapterItems); 392 position++; 393 } 394 395 // Split of private space apps into user-installed and system apps. 396 Map<Boolean, List<AppInfo>> split = mPrivateApps.stream() 397 .collect(Collectors.partitioningBy(mPrivateProviderManager 398 .splitIntoUserInstalledAndSystemApps(mActivityContext))); 399 400 // TODO(b/329688630): switch to the pulled LayoutStaticSnapshot atom 401 mActivityContext 402 .getStatsLogManager() 403 .logger() 404 .withCardinality(split.get(true).size()) 405 .log(LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT); 406 407 mActivityContext 408 .getStatsLogManager() 409 .logger() 410 .withCardinality(split.get(false).size()) 411 .log(LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT); 412 413 // Add user installed apps 414 position = addAppsWithSections(split.get(true), position); 415 // Add system apps separator. 416 if (Flags.privateSpaceSysAppsSeparation()) { 417 position = mPrivateProviderManager.addSystemAppsDivider(mAdapterItems); 418 if (Flags.letterFastScroller()) { 419 FastScrollSectionInfo sectionInfo = 420 new FastScrollSectionInfo(mPrivateProfileDividerBadge, position); 421 mFastScrollerSections.add(sectionInfo); 422 } 423 } 424 // Add system apps. 425 position = addAppsWithSections(split.get(false), position); 426 427 if (enableMovingContentIntoPrivateSpace()) { 428 // Look for the private space app via package and move it after header. 429 int headerIndex = -1; 430 int privateSpaceAppIndex = -1; 431 for (int i = 0; i < mAdapterItems.size(); i++) { 432 BaseAllAppsAdapter.AdapterItem currentItem = mAdapterItems.get(i); 433 if (currentItem.viewType == VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER) { 434 headerIndex = i; 435 } 436 if (currentItem.itemInfo != null && Objects.equals( 437 currentItem.itemInfo.getTargetPackage(), PRIVATE_SPACE_PACKAGE)) { 438 currentItem.itemInfo.bitmap = mPrivateProviderManager.preparePSBitmapInfo(); 439 currentItem.itemInfo.bitmap.creationFlags |= FLAG_NO_BADGE; 440 currentItem.itemInfo.contentDescription = 441 mPrivateProviderManager.getPsAppContentDesc(); 442 privateSpaceAppIndex = i; 443 } 444 } 445 if (headerIndex != -1 && privateSpaceAppIndex != -1) { 446 BaseAllAppsAdapter.AdapterItem movedItem = 447 mAdapterItems.remove(privateSpaceAppIndex); 448 // Move the icon after the header. 449 mAdapterItems.add(headerIndex + 1, movedItem); 450 } 451 } 452 return position; 453 } 454 addAppsWithSections(List<AppInfo> appList, int startPosition)455 private int addAppsWithSections(List<AppInfo> appList, int startPosition) { 456 String lastSectionName = null; 457 boolean hasPrivateApps = false; 458 int position = startPosition; 459 if (mPrivateProviderManager != null) { 460 hasPrivateApps = appList.stream(). 461 allMatch(mPrivateProviderManager.getItemInfoMatcher()); 462 } 463 Log.d(TAG, "Adding apps with sections. HasPrivateApps: " + hasPrivateApps); 464 for (int i = 0; i < appList.size(); i++) { 465 AppInfo info = appList.get(i); 466 // Apply decorator to private apps. 467 if (hasPrivateApps) { 468 mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info, 469 new SectionDecorationInfo(mActivityContext, 470 getRoundRegions(i, appList.size()), true /* decorateTogether */))); 471 } else { 472 mAdapterItems.add(AdapterItem.asApp(info)); 473 } 474 475 String sectionName = info.sectionName; 476 // Create a new section if the section names do not match 477 if (!sectionName.equals(lastSectionName)) { 478 Log.d(TAG, "addAppsWithSections: adding sectionName: " + sectionName 479 + " with appInfoTitle: " + info.title); 480 lastSectionName = sectionName; 481 boolean usePrivateAppScrollerBadge = !Flags.letterFastScroller() && hasPrivateApps; 482 FastScrollSectionInfo sectionInfo = new FastScrollSectionInfo( 483 usePrivateAppScrollerBadge ? 484 mPrivateProfileAppScrollerBadge : sectionName, position); 485 mFastScrollerSections.add(sectionInfo); 486 } 487 position++; 488 } 489 return position; 490 } 491 492 /** 493 * Determines the corner regions that should be rounded for a specific app icon based on its 494 * position in a grid. Apps that should only be cared about rounding are the apps in the last 495 * row. In the last row on the first column, the app should only be rounded on the bottom left. 496 * Apps in the middle would not be rounded and the last app on the last row will ALWAYS have a 497 * {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}. 498 * 499 * @param appIndex The index of the app icon within the app list. 500 * @param appListSize The total number of apps within the app list. 501 * @return An integer representing the corner regions to be rounded, using bitwise flags: 502 * - {@link SectionDecorationInfo#ROUND_NOTHING}: No corners should be rounded. 503 * - {@link SectionDecorationInfo#ROUND_TOP_LEFT}: Round the top-left corner. 504 * - {@link SectionDecorationInfo#ROUND_TOP_RIGHT}: Round the top-right corner. 505 * - {@link SectionDecorationInfo#ROUND_BOTTOM_LEFT}: Round the bottom-left corner. 506 * - {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}: Round the bottom-right corner. 507 */ 508 @VisibleForTesting getRoundRegions(int appIndex, int appListSize)509 int getRoundRegions(int appIndex, int appListSize) { 510 int numberOfAppRows = (int) Math.ceil((double) appListSize / mNumAppsPerRowAllApps); 511 int roundRegion = ROUND_NOTHING; 512 // App is in the last row. 513 if ((appIndex / mNumAppsPerRowAllApps) == numberOfAppRows - 1) { 514 if ((appIndex % mNumAppsPerRowAllApps) == 0) { 515 // App is the first column. 516 roundRegion = ROUND_BOTTOM_LEFT; 517 } else if ((appIndex % mNumAppsPerRowAllApps) == mNumAppsPerRowAllApps-1) { 518 // App is in the last column. 519 roundRegion = ROUND_BOTTOM_RIGHT; 520 } 521 // Ensure the last private app is rounded on the bottom right. 522 if (appIndex == appListSize - 1) { 523 roundRegion |= ROUND_BOTTOM_RIGHT; 524 } 525 } 526 return roundRegion; 527 } 528 getPrivateProfileManager()529 public PrivateProfileManager getPrivateProfileManager() { 530 return mPrivateProviderManager; 531 } 532 dump(String prefix, PrintWriter writer)533 public void dump(String prefix, PrintWriter writer) { 534 writer.println(prefix + "SectionInfo[] size: " + mFastScrollerSections.size()); 535 for (int i = 0; i < mFastScrollerSections.size(); i++) { 536 writer.println("\tFastScrollSection: " + mFastScrollerSections.get(i).sectionName); 537 } 538 } 539 540 private static class MyDiffCallback extends DiffUtil.Callback { 541 542 private final List<AdapterItem> mOldList; 543 private final List<AdapterItem> mNewList; 544 MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList)545 MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList) { 546 mOldList = oldList; 547 mNewList = newList; 548 } 549 550 @Override getOldListSize()551 public int getOldListSize() { 552 return mOldList.size(); 553 } 554 555 @Override getNewListSize()556 public int getNewListSize() { 557 return mNewList.size(); 558 } 559 560 @Override areItemsTheSame(int oldItemPosition, int newItemPosition)561 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 562 return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition)); 563 } 564 565 @Override areContentsTheSame(int oldItemPosition, int newItemPosition)566 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 567 return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition)); 568 } 569 } 570 571 }