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