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 android.content.Context; 19 import android.util.Log; 20 21 import com.android.launcher3.AppInfo; 22 import com.android.launcher3.Launcher; 23 import com.android.launcher3.compat.AlphabeticIndexCompat; 24 import com.android.launcher3.compat.UserHandleCompat; 25 import com.android.launcher3.config.ProviderConfig; 26 import com.android.launcher3.model.AppNameComparator; 27 import com.android.launcher3.util.ComponentKey; 28 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Locale; 34 import java.util.Map; 35 import java.util.TreeMap; 36 37 /** 38 * The alphabetically sorted list of applications. 39 */ 40 public class AlphabeticalAppsList { 41 42 public static final String TAG = "AlphabeticalAppsList"; 43 private static final boolean DEBUG = false; 44 private static final boolean DEBUG_PREDICTIONS = false; 45 46 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; 47 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; 48 49 private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; 50 51 /** 52 * Info about a section in the alphabetic list 53 */ 54 public static class SectionInfo { 55 // The number of applications in this section 56 public int numApps; 57 // The section break AdapterItem for this section 58 public AdapterItem sectionBreakItem; 59 // The first app AdapterItem for this section 60 public AdapterItem firstAppItem; 61 } 62 63 /** 64 * Info about a fast scroller section, depending if sections are merged, the fast scroller 65 * sections will not be the same set as the section headers. 66 */ 67 public static class FastScrollSectionInfo { 68 // The section name 69 public String sectionName; 70 // The AdapterItem to scroll to for this section 71 public AdapterItem fastScrollToItem; 72 // The touch fraction that should map to this fast scroll section info 73 public float touchFraction; 74 75 public FastScrollSectionInfo(String sectionName) { 76 this.sectionName = sectionName; 77 } 78 } 79 80 /** 81 * Info about a particular adapter item (can be either section or app) 82 */ 83 public static class AdapterItem { 84 /** Common properties */ 85 // The index of this adapter item in the list 86 public int position; 87 // The type of this item 88 public int viewType; 89 90 /** Section & App properties */ 91 // The section for this item 92 public SectionInfo sectionInfo; 93 94 /** App-only properties */ 95 // The section name of this app. Note that there can be multiple items with different 96 // sectionNames in the same section 97 public String sectionName = null; 98 // The index of this app in the section 99 public int sectionAppIndex = -1; 100 // The row that this item shows up on 101 public int rowIndex; 102 // The index of this app in the row 103 public int rowAppIndex; 104 // The associated AppInfo for the app 105 public AppInfo appInfo = null; 106 // The index of this app not including sections 107 public int appIndex = -1; 108 109 public static AdapterItem asSectionBreak(int pos, SectionInfo section) { 110 AdapterItem item = new AdapterItem(); 111 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK; 112 item.position = pos; 113 item.sectionInfo = section; 114 section.sectionBreakItem = item; 115 return item; 116 } 117 118 public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, 119 int sectionAppIndex, AppInfo appInfo, int appIndex) { 120 AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); 121 item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON; 122 return item; 123 } 124 125 public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, 126 int sectionAppIndex, AppInfo appInfo, int appIndex) { 127 AdapterItem item = new AdapterItem(); 128 item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON; 129 item.position = pos; 130 item.sectionInfo = section; 131 item.sectionName = sectionName; 132 item.sectionAppIndex = sectionAppIndex; 133 item.appInfo = appInfo; 134 item.appIndex = appIndex; 135 return item; 136 } 137 138 public static AdapterItem asEmptySearch(int pos) { 139 AdapterItem item = new AdapterItem(); 140 item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH; 141 item.position = pos; 142 return item; 143 } 144 145 public static AdapterItem asPredictionDivider(int pos) { 146 AdapterItem item = new AdapterItem(); 147 item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER; 148 item.position = pos; 149 return item; 150 } 151 152 public static AdapterItem asSearchDivder(int pos) { 153 AdapterItem item = new AdapterItem(); 154 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER; 155 item.position = pos; 156 return item; 157 } 158 159 public static AdapterItem asMarketDivider(int pos) { 160 AdapterItem item = new AdapterItem(); 161 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER; 162 item.position = pos; 163 return item; 164 } 165 166 public static AdapterItem asMarketSearch(int pos) { 167 AdapterItem item = new AdapterItem(); 168 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET; 169 item.position = pos; 170 return item; 171 } 172 } 173 174 /** 175 * Common interface for different merging strategies. 176 */ 177 public interface MergeAlgorithm { 178 boolean continueMerging(SectionInfo section, SectionInfo withSection, 179 int sectionAppCount, int numAppsPerRow, int mergeCount); 180 } 181 182 private Launcher mLauncher; 183 184 // The set of apps from the system not including predictions 185 private final List<AppInfo> mApps = new ArrayList<>(); 186 private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>(); 187 188 // The set of filtered apps with the current filter 189 private List<AppInfo> mFilteredApps = new ArrayList<>(); 190 // The current set of adapter items 191 private List<AdapterItem> mAdapterItems = new ArrayList<>(); 192 // The set of sections for the apps with the current filter 193 private List<SectionInfo> mSections = new ArrayList<>(); 194 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 195 private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 196 // The set of predicted app component names 197 private List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); 198 // The set of predicted apps resolved from the component names and the current set of apps 199 private List<AppInfo> mPredictedApps = new ArrayList<>(); 200 // The of ordered component names as a result of a search query 201 private ArrayList<ComponentKey> mSearchResults; 202 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 203 private AllAppsGridAdapter mAdapter; 204 private AlphabeticIndexCompat mIndexer; 205 private AppNameComparator mAppNameComparator; 206 private MergeAlgorithm mMergeAlgorithm; 207 private int mNumAppsPerRow; 208 private int mNumPredictedAppsPerRow; 209 private int mNumAppRowsInAdapter; 210 211 public AlphabeticalAppsList(Context context) { 212 mLauncher = Launcher.getLauncher(context); 213 mIndexer = new AlphabeticIndexCompat(context); 214 mAppNameComparator = new AppNameComparator(context); 215 } 216 217 /** 218 * Sets the number of apps per row. 219 */ 220 public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, 221 MergeAlgorithm mergeAlgorithm) { 222 mNumAppsPerRow = numAppsPerRow; 223 mNumPredictedAppsPerRow = numPredictedAppsPerRow; 224 mMergeAlgorithm = mergeAlgorithm; 225 226 updateAdapterItems(); 227 } 228 229 /** 230 * Sets the adapter to notify when this dataset changes. 231 */ 232 public void setAdapter(AllAppsGridAdapter adapter) { 233 mAdapter = adapter; 234 } 235 236 /** 237 * Returns all the apps. 238 */ 239 public List<AppInfo> getApps() { 240 return mApps; 241 } 242 243 /** 244 * Returns sections of all the current filtered applications. 245 */ 246 public List<SectionInfo> getSections() { 247 return mSections; 248 } 249 250 /** 251 * Returns fast scroller sections of all the current filtered applications. 252 */ 253 public List<FastScrollSectionInfo> getFastScrollerSections() { 254 return mFastScrollerSections; 255 } 256 257 /** 258 * Returns the current filtered list of applications broken down into their sections. 259 */ 260 public List<AdapterItem> getAdapterItems() { 261 return mAdapterItems; 262 } 263 264 /** 265 * Returns the number of rows of applications (not including predictions) 266 */ 267 public int getNumAppRows() { 268 return mNumAppRowsInAdapter; 269 } 270 271 /** 272 * Returns the number of applications in this list. 273 */ 274 public int getNumFilteredApps() { 275 return mFilteredApps.size(); 276 } 277 278 /** 279 * Returns whether there are is a filter set. 280 */ 281 public boolean hasFilter() { 282 return (mSearchResults != null); 283 } 284 285 /** 286 * Returns whether there are no filtered results. 287 */ 288 public boolean hasNoFilteredResults() { 289 return (mSearchResults != null) && mFilteredApps.isEmpty(); 290 } 291 292 /** 293 * Sets the sorted list of filtered components. 294 */ 295 public boolean setOrderedFilter(ArrayList<ComponentKey> f) { 296 if (mSearchResults != f) { 297 boolean same = mSearchResults != null && mSearchResults.equals(f); 298 mSearchResults = f; 299 updateAdapterItems(); 300 return !same; 301 } 302 return false; 303 } 304 305 /** 306 * Sets the current set of predicted apps. Since this can be called before we get the full set 307 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 308 */ 309 public void setPredictedApps(List<ComponentKey> apps) { 310 mPredictedAppComponents.clear(); 311 mPredictedAppComponents.addAll(apps); 312 onAppsUpdated(); 313 } 314 315 /** 316 * Sets the current set of apps. 317 */ 318 public void setApps(List<AppInfo> apps) { 319 mComponentToAppMap.clear(); 320 addApps(apps); 321 } 322 323 /** 324 * Adds new apps to the list. 325 */ 326 public void addApps(List<AppInfo> apps) { 327 updateApps(apps); 328 } 329 330 /** 331 * Updates existing apps in the list 332 */ 333 public void updateApps(List<AppInfo> apps) { 334 for (AppInfo app : apps) { 335 mComponentToAppMap.put(app.toComponentKey(), app); 336 } 337 onAppsUpdated(); 338 } 339 340 /** 341 * Removes some apps from the list. 342 */ 343 public void removeApps(List<AppInfo> apps) { 344 for (AppInfo app : apps) { 345 mComponentToAppMap.remove(app.toComponentKey()); 346 } 347 onAppsUpdated(); 348 } 349 350 /** 351 * Updates internals when the set of apps are updated. 352 */ 353 private void onAppsUpdated() { 354 // Sort the list of apps 355 mApps.clear(); 356 mApps.addAll(mComponentToAppMap.values()); 357 Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); 358 359 // As a special case for some languages (currently only Simplified Chinese), we may need to 360 // coalesce sections 361 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 362 TreeMap<String, ArrayList<AppInfo>> sectionMap = null; 363 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 364 if (localeRequiresSectionSorting) { 365 // Compute the section headers. We use a TreeMap with the section name comparator to 366 // ensure that the sections are ordered when we iterate over it later 367 sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); 368 for (AppInfo info : mApps) { 369 // Add the section to the cache 370 String sectionName = getAndUpdateCachedSectionName(info.title); 371 372 // Add it to the mapping 373 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 374 if (sectionApps == null) { 375 sectionApps = new ArrayList<>(); 376 sectionMap.put(sectionName, sectionApps); 377 } 378 sectionApps.add(info); 379 } 380 381 // Add each of the section apps to the list in order 382 List<AppInfo> allApps = new ArrayList<>(mApps.size()); 383 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 384 allApps.addAll(entry.getValue()); 385 } 386 387 mApps.clear(); 388 mApps.addAll(allApps); 389 } else { 390 // Just compute the section headers for use below 391 for (AppInfo info : mApps) { 392 // Add the section to the cache 393 getAndUpdateCachedSectionName(info.title); 394 } 395 } 396 397 // Recompose the set of adapter items from the current set of apps 398 updateAdapterItems(); 399 } 400 401 /** 402 * Updates the set of filtered apps with the current filter. At this point, we expect 403 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 404 */ 405 private void updateAdapterItems() { 406 SectionInfo lastSectionInfo = null; 407 String lastSectionName = null; 408 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 409 int position = 0; 410 int appIndex = 0; 411 412 // Prepare to update the list of sections, filtered apps, etc. 413 mFilteredApps.clear(); 414 mFastScrollerSections.clear(); 415 mAdapterItems.clear(); 416 mSections.clear(); 417 418 if (DEBUG_PREDICTIONS) { 419 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { 420 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 421 UserHandleCompat.myUserHandle())); 422 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 423 UserHandleCompat.myUserHandle())); 424 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 425 UserHandleCompat.myUserHandle())); 426 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 427 UserHandleCompat.myUserHandle())); 428 } 429 } 430 431 // Add the search divider 432 mAdapterItems.add(AdapterItem.asSearchDivder(position++)); 433 434 // Process the predicted app components 435 mPredictedApps.clear(); 436 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 437 for (ComponentKey ck : mPredictedAppComponents) { 438 AppInfo info = mComponentToAppMap.get(ck); 439 if (info != null) { 440 mPredictedApps.add(info); 441 } else { 442 if (ProviderConfig.IS_DOGFOOD_BUILD) { 443 Log.e(TAG, "Predicted app not found: " + ck); 444 } 445 } 446 // Stop at the number of predicted apps 447 if (mPredictedApps.size() == mNumPredictedAppsPerRow) { 448 break; 449 } 450 } 451 452 if (!mPredictedApps.isEmpty()) { 453 // Add a section for the predictions 454 lastSectionInfo = new SectionInfo(); 455 lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); 456 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 457 mSections.add(lastSectionInfo); 458 mFastScrollerSections.add(lastFastScrollerSectionInfo); 459 mAdapterItems.add(sectionItem); 460 461 // Add the predicted app items 462 for (AppInfo info : mPredictedApps) { 463 AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, 464 "", lastSectionInfo.numApps++, info, appIndex++); 465 if (lastSectionInfo.firstAppItem == null) { 466 lastSectionInfo.firstAppItem = appItem; 467 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 468 } 469 mAdapterItems.add(appItem); 470 mFilteredApps.add(info); 471 } 472 473 mAdapterItems.add(AdapterItem.asPredictionDivider(position++)); 474 } 475 } 476 477 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 478 // ordered set of sections 479 for (AppInfo info : getFiltersAppInfos()) { 480 String sectionName = getAndUpdateCachedSectionName(info.title); 481 482 // Create a new section if the section names do not match 483 if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { 484 lastSectionName = sectionName; 485 lastSectionInfo = new SectionInfo(); 486 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 487 mSections.add(lastSectionInfo); 488 mFastScrollerSections.add(lastFastScrollerSectionInfo); 489 490 // Create a new section item to break the flow of items in the list 491 if (!hasFilter()) { 492 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 493 mAdapterItems.add(sectionItem); 494 } 495 } 496 497 // Create an app item 498 AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, 499 lastSectionInfo.numApps++, info, appIndex++); 500 if (lastSectionInfo.firstAppItem == null) { 501 lastSectionInfo.firstAppItem = appItem; 502 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 503 } 504 mAdapterItems.add(appItem); 505 mFilteredApps.add(info); 506 } 507 508 // Append the search market item if we are currently searching 509 if (hasFilter()) { 510 if (hasNoFilteredResults()) { 511 mAdapterItems.add(AdapterItem.asEmptySearch(position++)); 512 } else { 513 mAdapterItems.add(AdapterItem.asMarketDivider(position++)); 514 } 515 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 516 } 517 518 // Merge multiple sections together as requested by the merge strategy for this device 519 mergeSections(); 520 521 if (mNumAppsPerRow != 0) { 522 // Update the number of rows in the adapter after we do all the merging (otherwise, we 523 // would have to shift the values again) 524 int numAppsInSection = 0; 525 int numAppsInRow = 0; 526 int rowIndex = -1; 527 for (AdapterItem item : mAdapterItems) { 528 item.rowIndex = 0; 529 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) { 530 numAppsInSection = 0; 531 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 532 if (numAppsInSection % mNumAppsPerRow == 0) { 533 numAppsInRow = 0; 534 rowIndex++; 535 } 536 item.rowIndex = rowIndex; 537 item.rowAppIndex = numAppsInRow; 538 numAppsInSection++; 539 numAppsInRow++; 540 } 541 } 542 mNumAppRowsInAdapter = rowIndex + 1; 543 544 // Pre-calculate all the fast scroller fractions 545 switch (mFastScrollDistributionMode) { 546 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: 547 float rowFraction = 1f / mNumAppRowsInAdapter; 548 for (FastScrollSectionInfo info : mFastScrollerSections) { 549 AdapterItem item = info.fastScrollToItem; 550 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 551 info.touchFraction = 0f; 552 continue; 553 } 554 555 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 556 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 557 } 558 break; 559 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: 560 float perSectionTouchFraction = 1f / mFastScrollerSections.size(); 561 float cumulativeTouchFraction = 0f; 562 for (FastScrollSectionInfo info : mFastScrollerSections) { 563 AdapterItem item = info.fastScrollToItem; 564 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 565 info.touchFraction = 0f; 566 continue; 567 } 568 info.touchFraction = cumulativeTouchFraction; 569 cumulativeTouchFraction += perSectionTouchFraction; 570 } 571 break; 572 } 573 } 574 575 // Refresh the recycler view 576 if (mAdapter != null) { 577 mAdapter.notifyDataSetChanged(); 578 } 579 } 580 581 private List<AppInfo> getFiltersAppInfos() { 582 if (mSearchResults == null) { 583 return mApps; 584 } 585 586 ArrayList<AppInfo> result = new ArrayList<>(); 587 for (ComponentKey key : mSearchResults) { 588 AppInfo match = mComponentToAppMap.get(key); 589 if (match != null) { 590 result.add(match); 591 } 592 } 593 return result; 594 } 595 596 /** 597 * Merges multiple sections to reduce visual raggedness. 598 */ 599 private void mergeSections() { 600 // Ignore merging until we have an algorithm and a valid row size 601 if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { 602 return; 603 } 604 605 // Go through each section and try and merge some of the sections 606 if (!hasFilter()) { 607 int sectionAppCount = 0; 608 for (int i = 0; i < mSections.size() - 1; i++) { 609 SectionInfo section = mSections.get(i); 610 sectionAppCount = section.numApps; 611 int mergeCount = 1; 612 613 // Merge rows based on the current strategy 614 while (i < (mSections.size() - 1) && 615 mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), 616 sectionAppCount, mNumAppsPerRow, mergeCount)) { 617 SectionInfo nextSection = mSections.remove(i + 1); 618 619 // Remove the next section break 620 mAdapterItems.remove(nextSection.sectionBreakItem); 621 int pos = mAdapterItems.indexOf(section.firstAppItem); 622 623 // Point the section for these new apps to the merged section 624 int nextPos = pos + section.numApps; 625 for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { 626 AdapterItem item = mAdapterItems.get(j); 627 item.sectionInfo = section; 628 item.sectionAppIndex += section.numApps; 629 } 630 631 // Update the following adapter items of the removed section item 632 pos = mAdapterItems.indexOf(nextSection.firstAppItem); 633 for (int j = pos; j < mAdapterItems.size(); j++) { 634 AdapterItem item = mAdapterItems.get(j); 635 item.position--; 636 } 637 section.numApps += nextSection.numApps; 638 sectionAppCount += nextSection.numApps; 639 640 if (DEBUG) { 641 Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + 642 " to " + section.firstAppItem.sectionName + 643 " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); 644 } 645 mergeCount++; 646 } 647 } 648 } 649 } 650 651 /** 652 * Returns the cached section name for the given title, recomputing and updating the cache if 653 * the title has no cached section name. 654 */ 655 private String getAndUpdateCachedSectionName(CharSequence title) { 656 String sectionName = mCachedSectionNames.get(title); 657 if (sectionName == null) { 658 sectionName = mIndexer.computeSectionName(title); 659 mCachedSectionNames.put(title, sectionName); 660 } 661 return sectionName; 662 } 663 } 664