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