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