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 */ 16 package com.android.launcher3.allapps; 17 18 import android.content.Context; 19 import android.os.Process; 20 import android.support.annotation.NonNull; 21 import android.support.annotation.Nullable; 22 import android.util.Log; 23 24 import com.android.launcher3.AppInfo; 25 import com.android.launcher3.Launcher; 26 import com.android.launcher3.compat.AlphabeticIndexCompat; 27 import com.android.launcher3.config.ProviderConfig; 28 import com.android.launcher3.discovery.AppDiscoveryAppInfo; 29 import com.android.launcher3.discovery.AppDiscoveryItem; 30 import com.android.launcher3.discovery.AppDiscoveryUpdateState; 31 import com.android.launcher3.util.ComponentKey; 32 import com.android.launcher3.util.LabelComparator; 33 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.TreeMap; 41 42 /** 43 * The alphabetically sorted list of applications. 44 */ 45 public 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 private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>(); 187 188 // The of ordered component names as a result of a search query 189 private ArrayList<ComponentKey> mSearchResults; 190 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 191 private AllAppsGridAdapter mAdapter; 192 private AlphabeticIndexCompat mIndexer; 193 private AppInfoComparator mAppNameComparator; 194 private int mNumAppsPerRow; 195 private int mNumPredictedAppsPerRow; 196 private int mNumAppRowsInAdapter; 197 198 public AlphabeticalAppsList(Context context) { 199 mLauncher = Launcher.getLauncher(context); 200 mIndexer = new AlphabeticIndexCompat(context); 201 mAppNameComparator = new AppInfoComparator(context); 202 } 203 204 /** 205 * Sets the number of apps per row. 206 */ 207 public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) { 208 mNumAppsPerRow = numAppsPerRow; 209 mNumPredictedAppsPerRow = numPredictedAppsPerRow; 210 211 updateAdapterItems(); 212 } 213 214 /** 215 * Sets the adapter to notify when this dataset changes. 216 */ 217 public void setAdapter(AllAppsGridAdapter adapter) { 218 mAdapter = adapter; 219 } 220 221 /** 222 * Returns all the apps. 223 */ 224 public List<AppInfo> getApps() { 225 return mApps; 226 } 227 228 /** 229 * Returns fast scroller sections of all the current filtered applications. 230 */ 231 public List<FastScrollSectionInfo> getFastScrollerSections() { 232 return mFastScrollerSections; 233 } 234 235 /** 236 * Returns the current filtered list of applications broken down into their sections. 237 */ 238 public List<AdapterItem> getAdapterItems() { 239 return mAdapterItems; 240 } 241 242 /** 243 * Returns the number of rows of applications (not including predictions) 244 */ 245 public int getNumAppRows() { 246 return mNumAppRowsInAdapter; 247 } 248 249 /** 250 * Returns the number of applications in this list. 251 */ 252 public int getNumFilteredApps() { 253 return mFilteredApps.size(); 254 } 255 256 /** 257 * Returns whether there are is a filter set. 258 */ 259 public boolean hasFilter() { 260 return (mSearchResults != null); 261 } 262 263 /** 264 * Returns whether there are no filtered results. 265 */ 266 public boolean hasNoFilteredResults() { 267 return (mSearchResults != null) && mFilteredApps.isEmpty(); 268 } 269 270 boolean shouldShowEmptySearch() { 271 return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty(); 272 } 273 274 /** 275 * Sets the sorted list of filtered components. 276 */ 277 public boolean setOrderedFilter(ArrayList<ComponentKey> f) { 278 if (mSearchResults != f) { 279 boolean same = mSearchResults != null && mSearchResults.equals(f); 280 mSearchResults = f; 281 updateAdapterItems(); 282 return !same; 283 } 284 return false; 285 } 286 287 public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app, 288 @NonNull AppDiscoveryUpdateState state) { 289 mAppDiscoveryUpdateState = state; 290 switch (state) { 291 case START: 292 mDiscoveredApps.clear(); 293 break; 294 case UPDATE: 295 mDiscoveredApps.add(new AppDiscoveryAppInfo(app)); 296 break; 297 } 298 updateAdapterItems(); 299 } 300 301 /** 302 * Sets the current set of predicted apps. Since this can be called before we get the full set 303 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 304 */ 305 public void setPredictedApps(List<ComponentKey> apps) { 306 mPredictedAppComponents.clear(); 307 mPredictedAppComponents.addAll(apps); 308 onAppsUpdated(); 309 } 310 311 /** 312 * Sets the current set of apps. 313 */ 314 public void setApps(List<AppInfo> apps) { 315 mComponentToAppMap.clear(); 316 addApps(apps); 317 } 318 319 /** 320 * Adds new apps to the list. 321 */ 322 public void addApps(List<AppInfo> apps) { 323 updateApps(apps); 324 } 325 326 /** 327 * Updates existing apps in the list 328 */ 329 public void updateApps(List<AppInfo> apps) { 330 for (AppInfo app : apps) { 331 mComponentToAppMap.put(app.toComponentKey(), app); 332 } 333 onAppsUpdated(); 334 } 335 336 /** 337 * Removes some apps from the list. 338 */ 339 public void removeApps(List<AppInfo> apps) { 340 for (AppInfo app : apps) { 341 mComponentToAppMap.remove(app.toComponentKey()); 342 } 343 onAppsUpdated(); 344 } 345 346 /** 347 * Updates internals when the set of apps are updated. 348 */ 349 private void onAppsUpdated() { 350 // Sort the list of apps 351 mApps.clear(); 352 mApps.addAll(mComponentToAppMap.values()); 353 Collections.sort(mApps, mAppNameComparator); 354 355 // As a special case for some languages (currently only Simplified Chinese), we may need to 356 // coalesce sections 357 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 358 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 359 if (localeRequiresSectionSorting) { 360 // Compute the section headers. We use a TreeMap with the section name comparator to 361 // ensure that the sections are ordered when we iterate over it later 362 TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator()); 363 for (AppInfo info : mApps) { 364 // Add the section to the cache 365 String sectionName = getAndUpdateCachedSectionName(info.title); 366 367 // Add it to the mapping 368 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 369 if (sectionApps == null) { 370 sectionApps = new ArrayList<>(); 371 sectionMap.put(sectionName, sectionApps); 372 } 373 sectionApps.add(info); 374 } 375 376 // Add each of the section apps to the list in order 377 mApps.clear(); 378 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 379 mApps.addAll(entry.getValue()); 380 } 381 } else { 382 // Just compute the section headers for use below 383 for (AppInfo info : mApps) { 384 // Add the section to the cache 385 getAndUpdateCachedSectionName(info.title); 386 } 387 } 388 389 // Recompose the set of adapter items from the current set of apps 390 updateAdapterItems(); 391 } 392 393 /** 394 * Updates the set of filtered apps with the current filter. At this point, we expect 395 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 396 */ 397 private void updateAdapterItems() { 398 refillAdapterItems(); 399 refreshRecyclerView(); 400 } 401 402 private void refreshRecyclerView() { 403 if (mAdapter != null) { 404 mAdapter.notifyDataSetChanged(); 405 } 406 } 407 408 private void refillAdapterItems() { 409 String lastSectionName = null; 410 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 411 int position = 0; 412 int appIndex = 0; 413 414 // Prepare to update the list of sections, filtered apps, etc. 415 mFilteredApps.clear(); 416 mFastScrollerSections.clear(); 417 mAdapterItems.clear(); 418 419 if (DEBUG_PREDICTIONS) { 420 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { 421 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 422 Process.myUserHandle())); 423 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 424 Process.myUserHandle())); 425 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 426 Process.myUserHandle())); 427 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 428 Process.myUserHandle())); 429 } 430 } 431 432 // Add the search divider 433 mAdapterItems.add(AdapterItem.asSearchDivider(position++)); 434 435 // Process the predicted app components 436 mPredictedApps.clear(); 437 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 438 for (ComponentKey ck : mPredictedAppComponents) { 439 AppInfo info = mComponentToAppMap.get(ck); 440 if (info != null) { 441 mPredictedApps.add(info); 442 } else { 443 if (ProviderConfig.IS_DOGFOOD_BUILD) { 444 Log.e(TAG, "Predicted app not found: " + ck); 445 } 446 } 447 // Stop at the number of predicted apps 448 if (mPredictedApps.size() == mNumPredictedAppsPerRow) { 449 break; 450 } 451 } 452 453 if (!mPredictedApps.isEmpty()) { 454 // Add a section for the predictions 455 lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); 456 mFastScrollerSections.add(lastFastScrollerSectionInfo); 457 458 // Add the predicted app items 459 for (AppInfo info : mPredictedApps) { 460 AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info, 461 appIndex++); 462 if (lastFastScrollerSectionInfo.fastScrollToItem == null) { 463 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 464 } 465 mAdapterItems.add(appItem); 466 mFilteredApps.add(info); 467 } 468 469 mAdapterItems.add(AdapterItem.asPredictionDivider(position++)); 470 } 471 } 472 473 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 474 // ordered set of sections 475 for (AppInfo info : getFiltersAppInfos()) { 476 String sectionName = getAndUpdateCachedSectionName(info.title); 477 478 // Create a new section if the section names do not match 479 if (!sectionName.equals(lastSectionName)) { 480 lastSectionName = sectionName; 481 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 482 mFastScrollerSections.add(lastFastScrollerSectionInfo); 483 } 484 485 // Create an app item 486 AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++); 487 if (lastFastScrollerSectionInfo.fastScrollToItem == null) { 488 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 489 } 490 mAdapterItems.add(appItem); 491 mFilteredApps.add(info); 492 } 493 494 if (hasFilter()) { 495 if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) { 496 mAdapterItems.add(AdapterItem.asLoadingDivider(position++)); 497 // Append all app discovery results 498 for (int i = 0; i < mDiscoveredApps.size(); i++) { 499 AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i); 500 if (appDiscoveryAppInfo.isRecent) { 501 // already handled in getFilteredAppInfos() 502 continue; 503 } 504 AdapterItem item = AdapterItem.asDiscoveryItem(position++, 505 "", appDiscoveryAppInfo, appIndex++); 506 mAdapterItems.add(item); 507 } 508 509 if (!isAppDiscoveryRunning()) { 510 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 511 } 512 } else { 513 // Append the search market item 514 if (hasNoFilteredResults()) { 515 mAdapterItems.add(AdapterItem.asEmptySearch(position++)); 516 } else { 517 mAdapterItems.add(AdapterItem.asMarketDivider(position++)); 518 } 519 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 520 } 521 } 522 523 if (mNumAppsPerRow != 0) { 524 // Update the number of rows in the adapter after we do all the merging (otherwise, we 525 // would have to shift the values again) 526 int numAppsInSection = 0; 527 int numAppsInRow = 0; 528 int rowIndex = -1; 529 for (AdapterItem item : mAdapterItems) { 530 item.rowIndex = 0; 531 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) { 532 numAppsInSection = 0; 533 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 534 if (numAppsInSection % mNumAppsPerRow == 0) { 535 numAppsInRow = 0; 536 rowIndex++; 537 } 538 item.rowIndex = rowIndex; 539 item.rowAppIndex = numAppsInRow; 540 numAppsInSection++; 541 numAppsInRow++; 542 } 543 } 544 mNumAppRowsInAdapter = rowIndex + 1; 545 546 // Pre-calculate all the fast scroller fractions 547 switch (mFastScrollDistributionMode) { 548 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: 549 float rowFraction = 1f / mNumAppRowsInAdapter; 550 for (FastScrollSectionInfo info : mFastScrollerSections) { 551 AdapterItem item = info.fastScrollToItem; 552 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 553 info.touchFraction = 0f; 554 continue; 555 } 556 557 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 558 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 559 } 560 break; 561 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: 562 float perSectionTouchFraction = 1f / mFastScrollerSections.size(); 563 float cumulativeTouchFraction = 0f; 564 for (FastScrollSectionInfo info : mFastScrollerSections) { 565 AdapterItem item = info.fastScrollToItem; 566 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 567 info.touchFraction = 0f; 568 continue; 569 } 570 info.touchFraction = cumulativeTouchFraction; 571 cumulativeTouchFraction += perSectionTouchFraction; 572 } 573 break; 574 } 575 } 576 } 577 578 public boolean isAppDiscoveryRunning() { 579 return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START 580 || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE; 581 } 582 583 private List<AppInfo> getFiltersAppInfos() { 584 if (mSearchResults == null) { 585 return mApps; 586 } 587 588 ArrayList<AppInfo> result = new ArrayList<>(); 589 for (ComponentKey key : mSearchResults) { 590 AppInfo match = mComponentToAppMap.get(key); 591 if (match != null) { 592 result.add(match); 593 } 594 } 595 596 // adding recently used instant apps 597 if (mDiscoveredApps.size() > 0) { 598 for (int i = 0; i < mDiscoveredApps.size(); i++) { 599 AppDiscoveryAppInfo discoveryAppInfo = mDiscoveredApps.get(i); 600 if (discoveryAppInfo.isRecent) { 601 result.add(discoveryAppInfo); 602 } 603 } 604 Collections.sort(result, mAppNameComparator); 605 } 606 return result; 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 } 623