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