AlphabeticalAppsList.java revision c0372a42a256421199e5d41592b69a6f4c2e9ef1
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.os.Process; 20import android.support.annotation.NonNull; 21import android.support.annotation.Nullable; 22import android.util.Log; 23 24import com.android.launcher3.AppInfo; 25import com.android.launcher3.Launcher; 26import com.android.launcher3.compat.AlphabeticIndexCompat; 27import com.android.launcher3.config.FeatureFlags; 28import com.android.launcher3.discovery.AppDiscoveryAppInfo; 29import com.android.launcher3.discovery.AppDiscoveryItem; 30import com.android.launcher3.discovery.AppDiscoveryUpdateState; 31import com.android.launcher3.util.ComponentKey; 32import com.android.launcher3.util.LabelComparator; 33 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 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; 52 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; 53 54 private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; 55 56 private AppDiscoveryUpdateState mAppDiscoveryUpdateState; 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 /** App-only properties */ 86 // The section name of this app. Note that there can be multiple items with different 87 // sectionNames in the same section 88 public String sectionName = null; 89 // The row that this item shows up on 90 public int rowIndex; 91 // The index of this app in the row 92 public int rowAppIndex; 93 // The associated AppInfo for the app 94 public AppInfo appInfo = null; 95 // The index of this app not including sections 96 public int appIndex = -1; 97 98 public static AdapterItem asPredictedApp(int pos, String sectionName, AppInfo appInfo, 99 int appIndex) { 100 AdapterItem item = asApp(pos, sectionName, appInfo, appIndex); 101 item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON; 102 return item; 103 } 104 105 public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, 106 int appIndex) { 107 AdapterItem item = new AdapterItem(); 108 item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON; 109 item.position = pos; 110 item.sectionName = sectionName; 111 item.appInfo = appInfo; 112 item.appIndex = appIndex; 113 return item; 114 } 115 116 public static AdapterItem asDiscoveryItem(int pos, String sectionName, AppInfo appInfo, 117 int appIndex) { 118 AdapterItem item = new AdapterItem(); 119 item.viewType = AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM; 120 item.position = pos; 121 item.sectionName = sectionName; 122 item.appInfo = appInfo; 123 item.appIndex = appIndex; 124 return item; 125 } 126 127 public static AdapterItem asEmptySearch(int pos) { 128 AdapterItem item = new AdapterItem(); 129 item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH; 130 item.position = pos; 131 return item; 132 } 133 134 public static AdapterItem asPredictionDivider(int pos) { 135 AdapterItem item = new AdapterItem(); 136 item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER; 137 item.position = pos; 138 return item; 139 } 140 141 public static AdapterItem asSearchDivider(int pos) { 142 AdapterItem item = new AdapterItem(); 143 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER; 144 item.position = pos; 145 return item; 146 } 147 148 public static AdapterItem asMarketDivider(int pos) { 149 AdapterItem item = new AdapterItem(); 150 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER; 151 item.position = pos; 152 return item; 153 } 154 155 public static AdapterItem asLoadingDivider(int pos) { 156 AdapterItem item = new AdapterItem(); 157 item.viewType = AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER; 158 item.position = pos; 159 return item; 160 } 161 162 public static AdapterItem asMarketSearch(int pos) { 163 AdapterItem item = new AdapterItem(); 164 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET; 165 item.position = pos; 166 return item; 167 } 168 } 169 170 private final Launcher mLauncher; 171 172 // The set of apps from the system not including predictions 173 private final List<AppInfo> mApps = new ArrayList<>(); 174 private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>(); 175 176 // The set of filtered apps with the current filter 177 private final List<AppInfo> mFilteredApps = new ArrayList<>(); 178 // The current set of adapter items 179 private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>(); 180 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 181 private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 182 // The set of predicted app component names 183 private final List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); 184 // The set of predicted apps resolved from the component names and the current set of apps 185 private final List<AppInfo> mPredictedApps = new ArrayList<>(); 186 // The set of apps returned from a discovery service while searching 187 private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>(); 188 // The suggested app returned by a discovery service 189 private AppDiscoveryAppInfo mSuggestedApp; 190 191 // The of ordered component names as a result of a search query 192 private ArrayList<ComponentKey> mSearchResults; 193 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 194 private AllAppsGridAdapter mAdapter; 195 private AlphabeticIndexCompat mIndexer; 196 private AppInfoComparator mAppNameComparator; 197 private int mNumAppsPerRow; 198 private int mNumPredictedAppsPerRow; 199 private int mNumAppRowsInAdapter; 200 201 public AlphabeticalAppsList(Context context) { 202 mLauncher = Launcher.getLauncher(context); 203 mIndexer = new AlphabeticIndexCompat(context); 204 mAppNameComparator = new AppInfoComparator(context); 205 } 206 207 /** 208 * Sets the number of apps per row. 209 */ 210 public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) { 211 mNumAppsPerRow = numAppsPerRow; 212 mNumPredictedAppsPerRow = numPredictedAppsPerRow; 213 214 updateAdapterItems(); 215 } 216 217 /** 218 * Sets the adapter to notify when this dataset changes. 219 */ 220 public void setAdapter(AllAppsGridAdapter adapter) { 221 mAdapter = adapter; 222 } 223 224 /** 225 * Returns all the apps. 226 */ 227 public List<AppInfo> getApps() { 228 return mApps; 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 boolean shouldShowEmptySearch() { 274 return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty(); 275 } 276 277 /** 278 * Sets the sorted list of filtered components. 279 */ 280 public boolean setOrderedFilter(ArrayList<ComponentKey> f) { 281 if (mSearchResults != f) { 282 boolean same = mSearchResults != null && mSearchResults.equals(f); 283 mSearchResults = f; 284 updateAdapterItems(); 285 return !same; 286 } 287 return false; 288 } 289 290 public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, 291 @NonNull AppDiscoveryUpdateState state) { 292 mAppDiscoveryUpdateState = state; 293 switch (state) { 294 case SUGGESTED: 295 mSuggestedApp = app != null ? new AppDiscoveryAppInfo(app) : null; 296 break; 297 case START: 298 mDiscoveredApps.clear(); 299 break; 300 case UPDATE: 301 mDiscoveredApps.add(new AppDiscoveryAppInfo(app)); 302 break; 303 } 304 updateAdapterItems(); 305 } 306 307 /** 308 * Sets the current set of predicted apps. Since this can be called before we get the full set 309 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 310 */ 311 public void setPredictedApps(List<ComponentKey> apps) { 312 mPredictedAppComponents.clear(); 313 mPredictedAppComponents.addAll(apps); 314 onAppsUpdated(); 315 } 316 317 /** 318 * Sets the current set of apps. 319 */ 320 public void setApps(List<AppInfo> apps) { 321 mComponentToAppMap.clear(); 322 addApps(apps); 323 } 324 325 /** 326 * Adds new apps to the list. 327 */ 328 public void addApps(List<AppInfo> apps) { 329 updateApps(apps); 330 } 331 332 /** 333 * Updates existing apps in the list 334 */ 335 public void updateApps(List<AppInfo> apps) { 336 for (AppInfo app : apps) { 337 mComponentToAppMap.put(app.toComponentKey(), app); 338 } 339 onAppsUpdated(); 340 } 341 342 /** 343 * Removes some apps from the list. 344 */ 345 public void removeApps(List<AppInfo> apps) { 346 for (AppInfo app : apps) { 347 mComponentToAppMap.remove(app.toComponentKey()); 348 } 349 onAppsUpdated(); 350 } 351 352 /** 353 * Updates internals when the set of apps are updated. 354 */ 355 private void onAppsUpdated() { 356 // Sort the list of apps 357 mApps.clear(); 358 mApps.addAll(mComponentToAppMap.values()); 359 Collections.sort(mApps, mAppNameComparator); 360 361 // As a special case for some languages (currently only Simplified Chinese), we may need to 362 // coalesce sections 363 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 364 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 365 if (localeRequiresSectionSorting) { 366 // Compute the section headers. We use a TreeMap with the section name comparator to 367 // ensure that the sections are ordered when we iterate over it later 368 TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator()); 369 for (AppInfo info : mApps) { 370 // Add the section to the cache 371 String sectionName = getAndUpdateCachedSectionName(info.title); 372 373 // Add it to the mapping 374 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 375 if (sectionApps == null) { 376 sectionApps = new ArrayList<>(); 377 sectionMap.put(sectionName, sectionApps); 378 } 379 sectionApps.add(info); 380 } 381 382 // Add each of the section apps to the list in order 383 mApps.clear(); 384 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 385 mApps.addAll(entry.getValue()); 386 } 387 } else { 388 // Just compute the section headers for use below 389 for (AppInfo info : mApps) { 390 // Add the section to the cache 391 getAndUpdateCachedSectionName(info.title); 392 } 393 } 394 395 // Recompose the set of adapter items from the current set of apps 396 updateAdapterItems(); 397 } 398 399 /** 400 * Updates the set of filtered apps with the current filter. At this point, we expect 401 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 402 */ 403 private void updateAdapterItems() { 404 refillAdapterItems(); 405 refreshRecyclerView(); 406 } 407 408 private void refreshRecyclerView() { 409 if (mAdapter != null) { 410 mAdapter.notifyDataSetChanged(); 411 } 412 } 413 414 private void refillAdapterItems() { 415 String lastSectionName = null; 416 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 417 int position = 0; 418 int appIndex = 0; 419 420 // Prepare to update the list of sections, filtered apps, etc. 421 mFilteredApps.clear(); 422 mFastScrollerSections.clear(); 423 mAdapterItems.clear(); 424 425 if (DEBUG_PREDICTIONS) { 426 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { 427 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 428 Process.myUserHandle())); 429 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 430 Process.myUserHandle())); 431 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 432 Process.myUserHandle())); 433 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 434 Process.myUserHandle())); 435 } 436 } 437 438 // Add the search divider 439 mAdapterItems.add(AdapterItem.asSearchDivider(position++)); 440 441 // Process the predicted app components 442 mPredictedApps.clear(); 443 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 444 int suggestedAppsCount = mSuggestedApp != null ? 1 : 0; 445 for (ComponentKey ck : mPredictedAppComponents) { 446 AppInfo info = mComponentToAppMap.get(ck); 447 if (info != null) { 448 mPredictedApps.add(info); 449 } else { 450 if (FeatureFlags.IS_DOGFOOD_BUILD) { 451 Log.e(TAG, "Predicted app not found: " + ck); 452 } 453 } 454 // Stop at the number of predicted apps 455 if (mPredictedApps.size() + suggestedAppsCount == mNumPredictedAppsPerRow) { 456 break; 457 } 458 } 459 460 if (mSuggestedApp != null) { 461 // adding suggested app as the last predicted app 462 mPredictedApps.add(mSuggestedApp); 463 } 464 465 if (!mPredictedApps.isEmpty()) { 466 // Add a section for the predictions 467 lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); 468 mFastScrollerSections.add(lastFastScrollerSectionInfo); 469 470 // Add the predicted app items 471 for (AppInfo info : mPredictedApps) { 472 AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info, 473 appIndex++); 474 if (lastFastScrollerSectionInfo.fastScrollToItem == null) { 475 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 476 } 477 mAdapterItems.add(appItem); 478 mFilteredApps.add(info); 479 } 480 481 mAdapterItems.add(AdapterItem.asPredictionDivider(position++)); 482 } 483 } 484 485 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 486 // ordered set of sections 487 for (AppInfo info : getFiltersAppInfos()) { 488 String sectionName = getAndUpdateCachedSectionName(info.title); 489 490 // Create a new section if the section names do not match 491 if (!sectionName.equals(lastSectionName)) { 492 lastSectionName = sectionName; 493 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 494 mFastScrollerSections.add(lastFastScrollerSectionInfo); 495 } 496 497 // Create an app item 498 AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++); 499 if (lastFastScrollerSectionInfo.fastScrollToItem == null) { 500 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 501 } 502 mAdapterItems.add(appItem); 503 mFilteredApps.add(info); 504 } 505 506 if (hasFilter()) { 507 if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) { 508 mAdapterItems.add(AdapterItem.asLoadingDivider(position++)); 509 // Append all app discovery results 510 for (int i = 0; i < mDiscoveredApps.size(); i++) { 511 AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i); 512 if (appDiscoveryAppInfo.isRecent) { 513 // already handled in getFilteredAppInfos() 514 continue; 515 } 516 AdapterItem item = AdapterItem.asDiscoveryItem(position++, 517 "", appDiscoveryAppInfo, appIndex++); 518 mAdapterItems.add(item); 519 } 520 521 if (!isAppDiscoveryRunning()) { 522 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 523 } 524 } else { 525 // Append the search market item 526 if (hasNoFilteredResults()) { 527 mAdapterItems.add(AdapterItem.asEmptySearch(position++)); 528 } else { 529 mAdapterItems.add(AdapterItem.asMarketDivider(position++)); 530 } 531 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 532 } 533 } 534 535 if (mNumAppsPerRow != 0) { 536 // Update the number of rows in the adapter after we do all the merging (otherwise, we 537 // would have to shift the values again) 538 int numAppsInSection = 0; 539 int numAppsInRow = 0; 540 int rowIndex = -1; 541 for (AdapterItem item : mAdapterItems) { 542 item.rowIndex = 0; 543 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) { 544 numAppsInSection = 0; 545 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 546 if (numAppsInSection % mNumAppsPerRow == 0) { 547 numAppsInRow = 0; 548 rowIndex++; 549 } 550 item.rowIndex = rowIndex; 551 item.rowAppIndex = numAppsInRow; 552 numAppsInSection++; 553 numAppsInRow++; 554 } 555 } 556 mNumAppRowsInAdapter = rowIndex + 1; 557 558 // Pre-calculate all the fast scroller fractions 559 switch (mFastScrollDistributionMode) { 560 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: 561 float rowFraction = 1f / mNumAppRowsInAdapter; 562 for (FastScrollSectionInfo info : mFastScrollerSections) { 563 AdapterItem item = info.fastScrollToItem; 564 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 565 info.touchFraction = 0f; 566 continue; 567 } 568 569 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 570 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 571 } 572 break; 573 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: 574 float perSectionTouchFraction = 1f / mFastScrollerSections.size(); 575 float cumulativeTouchFraction = 0f; 576 for (FastScrollSectionInfo info : mFastScrollerSections) { 577 AdapterItem item = info.fastScrollToItem; 578 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 579 info.touchFraction = 0f; 580 continue; 581 } 582 info.touchFraction = cumulativeTouchFraction; 583 cumulativeTouchFraction += perSectionTouchFraction; 584 } 585 break; 586 } 587 } 588 } 589 590 public boolean isAppDiscoveryRunning() { 591 return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START 592 || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE; 593 } 594 595 private List<AppInfo> getFiltersAppInfos() { 596 if (mSearchResults == null) { 597 return mApps; 598 } 599 600 ArrayList<AppInfo> result = new ArrayList<>(); 601 for (ComponentKey key : mSearchResults) { 602 AppInfo match = mComponentToAppMap.get(key); 603 if (match != null) { 604 result.add(match); 605 } 606 } 607 608 // adding recently used instant apps 609 if (mDiscoveredApps.size() > 0) { 610 for (int i = 0; i < mDiscoveredApps.size(); i++) { 611 AppDiscoveryAppInfo discoveryAppInfo = mDiscoveredApps.get(i); 612 if (discoveryAppInfo.isRecent) { 613 result.add(discoveryAppInfo); 614 } 615 } 616 Collections.sort(result, mAppNameComparator); 617 } 618 return result; 619 } 620 621 /** 622 * Returns the cached section name for the given title, recomputing and updating the cache if 623 * the title has no cached section name. 624 */ 625 private String getAndUpdateCachedSectionName(CharSequence title) { 626 String sectionName = mCachedSectionNames.get(title); 627 if (sectionName == null) { 628 sectionName = mIndexer.computeSectionName(title); 629 mCachedSectionNames.put(title, sectionName); 630 } 631 return sectionName; 632 } 633 634} 635