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