AlphabeticalAppsList.java revision dd5814c471c316091f59febcffedb8d58d693492
1package com.android.launcher3.allapps; 2 3import android.content.ComponentName; 4import android.content.Context; 5import android.support.v7.widget.RecyclerView; 6import android.util.Log; 7import com.android.launcher3.AppInfo; 8import com.android.launcher3.DeviceProfile; 9import com.android.launcher3.Launcher; 10import com.android.launcher3.compat.AlphabeticIndexCompat; 11import com.android.launcher3.model.AppNameComparator; 12 13import java.nio.charset.CharsetEncoder; 14import java.nio.charset.StandardCharsets; 15import java.util.ArrayList; 16import java.util.Collections; 17import java.util.HashMap; 18import java.util.List; 19import java.util.Locale; 20import java.util.Map; 21import java.util.TreeMap; 22 23/** 24 * The alphabetically sorted list of applications. 25 */ 26public class AlphabeticalAppsList { 27 28 public static final String TAG = "AlphabeticalAppsList"; 29 private static final boolean DEBUG = false; 30 31 /** 32 * Info about a section in the alphabetic list 33 */ 34 public static class SectionInfo { 35 // The number of applications in this section 36 public int numApps; 37 // The section break AdapterItem for this section 38 public AdapterItem sectionBreakItem; 39 // The first app AdapterItem for this section 40 public AdapterItem firstAppItem; 41 } 42 43 /** 44 * Info about a fast scroller section, depending if sections are merged, the fast scroller 45 * sections will not be the same set as the section headers. 46 */ 47 public static class FastScrollSectionInfo { 48 // The section name 49 public String sectionName; 50 // To map the touch (from 0..1) to the index in the app list to jump to in the fast 51 // scroller, we use the fraction in range (0..1) of the app index / total app count. 52 public float appRangeFraction; 53 // The AdapterItem to scroll to for this section 54 public AdapterItem appItem; 55 56 public FastScrollSectionInfo(String sectionName, float appRangeFraction) { 57 this.sectionName = sectionName; 58 this.appRangeFraction = appRangeFraction; 59 } 60 } 61 62 /** 63 * Info about a particular adapter item (can be either section or app) 64 */ 65 public static class AdapterItem { 66 /** Common properties */ 67 // The index of this adapter item in the list 68 public int position; 69 // The type of this item 70 public int viewType; 71 72 /** Section & App properties */ 73 // The section for this item 74 public SectionInfo sectionInfo; 75 76 /** App-only properties */ 77 // The section name of this app. Note that there can be multiple items with different 78 // sectionNames in the same section 79 public String sectionName = null; 80 // The index of this app in the section 81 public int sectionAppIndex = -1; 82 // The associated AppInfo for the app 83 public AppInfo appInfo = null; 84 // The index of this app not including sections 85 public int appIndex = -1; 86 87 public static AdapterItem asSectionBreak(int pos, SectionInfo section) { 88 AdapterItem item = new AdapterItem(); 89 item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE; 90 item.position = pos; 91 item.sectionInfo = section; 92 section.sectionBreakItem = item; 93 return item; 94 } 95 96 public static AdapterItem asPredictionBarSpacer(int pos) { 97 AdapterItem item = new AdapterItem(); 98 item.viewType = AllAppsGridAdapter.PREDICTION_BAR_SPACER_TYPE; 99 item.position = pos; 100 return item; 101 } 102 103 public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, 104 int sectionAppIndex, AppInfo appInfo, int appIndex) { 105 AdapterItem item = new AdapterItem(); 106 item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; 107 item.position = pos; 108 item.sectionInfo = section; 109 item.sectionName = sectionName; 110 item.sectionAppIndex = sectionAppIndex; 111 item.appInfo = appInfo; 112 item.appIndex = appIndex; 113 return item; 114 } 115 } 116 117 /** 118 * A filter interface to limit the set of applications in the apps list. 119 */ 120 public interface Filter { 121 boolean retainApp(AppInfo info, String sectionName); 122 } 123 124 /** 125 * Callback to notify when the set of adapter items have changed. 126 */ 127 public interface AdapterChangedCallback { 128 void onAdapterItemsChanged(); 129 } 130 131 /** 132 * Common interface for different merging strategies. 133 */ 134 private interface MergeAlgorithm { 135 boolean continueMerging(SectionInfo section, SectionInfo withSection, 136 int sectionAppCount, int numAppsPerRow, int mergeCount); 137 } 138 139 /** 140 * The logic we use to merge sections on tablets. Currently, we don't show section names on 141 * tablet layouts, so just merge all the sections indiscriminately. 142 */ 143 private static class TabletMergeAlgorithm implements MergeAlgorithm { 144 145 @Override 146 public boolean continueMerging(SectionInfo section, SectionInfo withSection, 147 int sectionAppCount, int numAppsPerRow, int mergeCount) { 148 // Merge EVERYTHING 149 return true; 150 } 151 } 152 153 /** 154 * The logic we use to merge sections on phones. We only merge sections when their final row 155 * contains less than a certain number of icons, and stop at a specified max number of merges. 156 * In addition, we will try and not merge sections that identify apps from different scripts. 157 */ 158 private static class PhoneMergeAlgorithm implements MergeAlgorithm { 159 160 private int mMinAppsPerRow; 161 private int mMinRowsInMergedSection; 162 private int mMaxAllowableMerges; 163 private CharsetEncoder mAsciiEncoder; 164 165 public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) { 166 mMinAppsPerRow = minAppsPerRow; 167 mMinRowsInMergedSection = minRowsInMergedSection; 168 mMaxAllowableMerges = maxNumMerges; 169 mAsciiEncoder = StandardCharsets.US_ASCII.newEncoder(); 170 } 171 172 @Override 173 public boolean continueMerging(SectionInfo section, SectionInfo withSection, 174 int sectionAppCount, int numAppsPerRow, int mergeCount) { 175 // Continue merging if the number of hanging apps on the final row is less than some 176 // fixed number (ragged), the merged rows has yet to exceed some minimum row count, 177 // and while the number of merged sections is less than some fixed number of merges 178 int rows = sectionAppCount / numAppsPerRow; 179 int cols = sectionAppCount % numAppsPerRow; 180 181 // Ensure that we do not merge across scripts, currently we only allow for english and 182 // native scripts so we can test if both can just be ascii encoded 183 boolean isCrossScript = false; 184 if (section.firstAppItem != null && withSection.firstAppItem != null) { 185 isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) != 186 mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName); 187 } 188 return (0 < cols && cols < mMinAppsPerRow) && 189 rows < mMinRowsInMergedSection && 190 mergeCount < mMaxAllowableMerges && 191 !isCrossScript; 192 } 193 } 194 195 private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3; 196 private static final int MAX_NUM_MERGES_PHONE = 2; 197 198 private Launcher mLauncher; 199 200 // The set of apps from the system not including predictions 201 private List<AppInfo> mApps = new ArrayList<>(); 202 // The set of filtered apps with the current filter 203 private List<AppInfo> mFilteredApps = new ArrayList<>(); 204 // The current set of adapter items 205 private List<AdapterItem> mAdapterItems = new ArrayList<>(); 206 // The set of sections for the apps with the current filter 207 private List<SectionInfo> mSections = new ArrayList<>(); 208 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 209 private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 210 // The set of predicted app component names 211 private List<ComponentName> mPredictedAppComponents = new ArrayList<>(); 212 // The set of predicted apps resolved from the component names and the current set of apps 213 private List<AppInfo> mPredictedApps = new ArrayList<>(); 214 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 215 private RecyclerView.Adapter mAdapter; 216 private Filter mFilter; 217 private AlphabeticIndexCompat mIndexer; 218 private AppNameComparator mAppNameComparator; 219 private MergeAlgorithm mMergeAlgorithm; 220 private AdapterChangedCallback mAdapterChangedCallback; 221 private int mNumAppsPerRow; 222 private int mNumPredictedAppsPerRow; 223 224 public AlphabeticalAppsList(Context context, int numAppsPerRow, int numPredictedAppsPerRow) { 225 mLauncher = (Launcher) context; 226 mIndexer = new AlphabeticIndexCompat(context); 227 mAppNameComparator = new AppNameComparator(context); 228 setNumAppsPerRow(numAppsPerRow, numPredictedAppsPerRow); 229 } 230 231 /** 232 * Sets the apps updated callback. 233 */ 234 public void setAdapterChangedCallback(AdapterChangedCallback cb) { 235 mAdapterChangedCallback = cb; 236 } 237 238 /** 239 * Sets the number of apps per row. Used only for AppsContainerView.SECTIONED_GRID_COALESCED. 240 */ 241 public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) { 242 // Update the merge algorithm 243 DeviceProfile grid = mLauncher.getDeviceProfile(); 244 if (grid.isPhone) { 245 mMergeAlgorithm = new PhoneMergeAlgorithm((int) Math.ceil(numAppsPerRow / 2f), 246 MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE); 247 } else { 248 mMergeAlgorithm = new TabletMergeAlgorithm(); 249 } 250 251 mNumAppsPerRow = numAppsPerRow; 252 mNumPredictedAppsPerRow = numPredictedAppsPerRow; 253 254 onAppsUpdated(); 255 } 256 257 /** 258 * Sets the adapter to notify when this dataset changes. 259 */ 260 public void setAdapter(RecyclerView.Adapter adapter) { 261 mAdapter = adapter; 262 } 263 264 /** 265 * Returns sections of all the current filtered applications. 266 */ 267 public List<SectionInfo> getSections() { 268 return mSections; 269 } 270 271 /** 272 * Returns fast scroller sections of all the current filtered applications. 273 */ 274 public List<FastScrollSectionInfo> getFastScrollerSections() { 275 return mFastScrollerSections; 276 } 277 278 /** 279 * Returns the current filtered list of applications broken down into their sections. 280 */ 281 public List<AdapterItem> getAdapterItems() { 282 return mAdapterItems; 283 } 284 285 /** 286 * Returns the number of applications in this list. 287 */ 288 public int getSize() { 289 return mFilteredApps.size(); 290 } 291 292 /** 293 * Returns whether there are is a filter set. 294 */ 295 public boolean hasFilter() { 296 return (mFilter != null); 297 } 298 299 /** 300 * Returns whether there are no filtered results. 301 */ 302 public boolean hasNoFilteredResults() { 303 return (mFilter != null) && mFilteredApps.isEmpty(); 304 } 305 306 /** 307 * Sets the current filter for this list of apps. 308 */ 309 public void setFilter(Filter f) { 310 if (mFilter != f) { 311 mFilter = f; 312 updateAdapterItems(); 313 } 314 } 315 316 /** 317 * Sets the current set of predicted apps. Since this can be called before we get the full set 318 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 319 */ 320 public void setPredictedApps(List<ComponentName> apps) { 321 mPredictedAppComponents.clear(); 322 mPredictedAppComponents.addAll(apps); 323 onAppsUpdated(); 324 } 325 326 /** 327 * Returns the current set of predicted apps. 328 */ 329 public List<AppInfo> getPredictedApps() { 330 return mPredictedApps; 331 } 332 333 /** 334 * Sets the current set of apps. 335 */ 336 public void setApps(List<AppInfo> apps) { 337 mApps.clear(); 338 mApps.addAll(apps); 339 onAppsUpdated(); 340 } 341 342 /** 343 * Adds new apps to the list. 344 */ 345 public void addApps(List<AppInfo> apps) { 346 // We add it in place, in alphabetical order 347 for (AppInfo info : apps) { 348 mApps.add(info); 349 } 350 onAppsUpdated(); 351 } 352 353 /** 354 * Updates existing apps in the list 355 */ 356 public void updateApps(List<AppInfo> apps) { 357 for (AppInfo info : apps) { 358 int index = mApps.indexOf(info); 359 if (index != -1) { 360 mApps.set(index, info); 361 } else { 362 mApps.add(info); 363 } 364 } 365 onAppsUpdated(); 366 } 367 368 /** 369 * Removes some apps from the list. 370 */ 371 public void removeApps(List<AppInfo> apps) { 372 for (AppInfo info : apps) { 373 int removeIndex = findAppByComponent(mApps, info); 374 if (removeIndex != -1) { 375 mApps.remove(removeIndex); 376 } 377 } 378 onAppsUpdated(); 379 } 380 381 /** 382 * Finds the index of an app given a target AppInfo. 383 */ 384 private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) { 385 ComponentName targetComponent = targetInfo.intent.getComponent(); 386 int length = apps.size(); 387 for (int i = 0; i < length; ++i) { 388 AppInfo info = apps.get(i); 389 if (info.user.equals(targetInfo.user) 390 && info.intent.getComponent().equals(targetComponent)) { 391 return i; 392 } 393 } 394 return -1; 395 } 396 397 /** 398 * Updates internals when the set of apps are updated. 399 */ 400 private void onAppsUpdated() { 401 // Sort the list of apps 402 Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); 403 404 // As a special case for some languages (currently only Simplified Chinese), we may need to 405 // coalesce sections 406 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 407 TreeMap<String, ArrayList<AppInfo>> sectionMap = null; 408 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 409 if (localeRequiresSectionSorting) { 410 // Compute the section headers. We use a TreeMap with the section name comparator to 411 // ensure that the sections are ordered when we iterate over it later 412 sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); 413 for (AppInfo info : mApps) { 414 // Add the section to the cache 415 String sectionName = getAndUpdateCachedSectionName(info.title); 416 417 // Add it to the mapping 418 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 419 if (sectionApps == null) { 420 sectionApps = new ArrayList<>(); 421 sectionMap.put(sectionName, sectionApps); 422 } 423 sectionApps.add(info); 424 } 425 426 // Add each of the section apps to the list in order 427 List<AppInfo> allApps = new ArrayList<>(mApps.size()); 428 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 429 allApps.addAll(entry.getValue()); 430 } 431 mApps = allApps; 432 } else { 433 // Just compute the section headers for use below 434 for (AppInfo info : mApps) { 435 // Add the section to the cache 436 getAndUpdateCachedSectionName(info.title); 437 } 438 } 439 440 // Recompose the set of adapter items from the current set of apps 441 updateAdapterItems(); 442 } 443 444 /** 445 * Updates the set of filtered apps with the current filter. At this point, we expect 446 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 447 */ 448 private void updateAdapterItems() { 449 SectionInfo lastSectionInfo = null; 450 String lastSectionName = null; 451 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 452 int position = 0; 453 int appIndex = 0; 454 455 // Prepare to update the list of sections, filtered apps, etc. 456 mFilteredApps.clear(); 457 mFastScrollerSections.clear(); 458 mAdapterItems.clear(); 459 mSections.clear(); 460 461 // Process the predicted app components 462 mPredictedApps.clear(); 463 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 464 for (ComponentName cn : mPredictedAppComponents) { 465 for (AppInfo info : mApps) { 466 if (cn.equals(info.componentName)) { 467 mPredictedApps.add(info); 468 break; 469 } 470 } 471 // Stop at the number of predicted apps 472 if (mPredictedApps.size() == mNumPredictedAppsPerRow) { 473 break; 474 } 475 } 476 477 if (!mPredictedApps.isEmpty()) { 478 // Create a new spacer for the prediction bar 479 AdapterItem sectionItem = AdapterItem.asPredictionBarSpacer(position++); 480 mAdapterItems.add(sectionItem); 481 } 482 } 483 484 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 485 // ordered set of sections 486 int numApps = mApps.size(); 487 for (int i = 0; i < numApps; i++) { 488 AppInfo info = mApps.get(i); 489 String sectionName = getAndUpdateCachedSectionName(info.title); 490 491 // Check if we want to retain this app 492 if (mFilter != null && !mFilter.retainApp(info, sectionName)) { 493 continue; 494 } 495 496 // Create a new section if the section names do not match 497 if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { 498 lastSectionName = sectionName; 499 lastSectionInfo = new SectionInfo(); 500 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName, 501 (float) appIndex / numApps); 502 mSections.add(lastSectionInfo); 503 mFastScrollerSections.add(lastFastScrollerSectionInfo); 504 505 // Create a new section item to break the flow of items in the list 506 if (!hasFilter()) { 507 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 508 mAdapterItems.add(sectionItem); 509 } 510 } 511 512 // Create an app item 513 AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, 514 lastSectionInfo.numApps++, info, appIndex++); 515 if (lastSectionInfo.firstAppItem == null) { 516 lastSectionInfo.firstAppItem = appItem; 517 lastFastScrollerSectionInfo.appItem = appItem; 518 } 519 mAdapterItems.add(appItem); 520 mFilteredApps.add(info); 521 } 522 523 // Merge multiple sections together as requested by the merge strategy for this device 524 mergeSections(); 525 526 // Refresh the recycler view 527 if (mAdapter != null) { 528 mAdapter.notifyDataSetChanged(); 529 } 530 531 if (mAdapterChangedCallback != null) { 532 mAdapterChangedCallback.onAdapterItemsChanged(); 533 } 534 } 535 536 /** 537 * Merges multiple sections to reduce visual raggedness. 538 */ 539 private void mergeSections() { 540 // Go through each section and try and merge some of the sections 541 if (AllAppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) { 542 int sectionAppCount = 0; 543 for (int i = 0; i < mSections.size() - 1; i++) { 544 SectionInfo section = mSections.get(i); 545 sectionAppCount = section.numApps; 546 int mergeCount = 1; 547 548 // Merge rows based on the current strategy 549 while (i < (mSections.size() - 1) && 550 mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), 551 sectionAppCount, mNumAppsPerRow, mergeCount)) { 552 SectionInfo nextSection = mSections.remove(i + 1); 553 554 // Remove the next section break 555 mAdapterItems.remove(nextSection.sectionBreakItem); 556 int pos = mAdapterItems.indexOf(section.firstAppItem); 557 // Point the section for these new apps to the merged section 558 int nextPos = pos + section.numApps; 559 for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { 560 AdapterItem item = mAdapterItems.get(j); 561 item.sectionInfo = section; 562 item.sectionAppIndex += section.numApps; 563 } 564 565 // Update the following adapter items of the removed section item 566 pos = mAdapterItems.indexOf(nextSection.firstAppItem); 567 for (int j = pos; j < mAdapterItems.size(); j++) { 568 AdapterItem item = mAdapterItems.get(j); 569 item.position--; 570 } 571 section.numApps += nextSection.numApps; 572 sectionAppCount += nextSection.numApps; 573 574 if (DEBUG) { 575 Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + 576 " to " + section.firstAppItem.sectionName + 577 " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); 578 } 579 mergeCount++; 580 } 581 } 582 } 583 } 584 585 /** 586 * Returns the cached section name for the given title, recomputing and updating the cache if 587 * the title has no cached section name. 588 */ 589 private String getAndUpdateCachedSectionName(CharSequence title) { 590 String sectionName = mCachedSectionNames.get(title); 591 if (sectionName == null) { 592 sectionName = mIndexer.computeSectionName(title); 593 mCachedSectionNames.put(title, sectionName); 594 } 595 return sectionName; 596 } 597} 598