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