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.support.v7.widget.RecyclerView; 20 import android.util.Log; 21 import com.android.launcher3.AppInfo; 22 import com.android.launcher3.Launcher; 23 import com.android.launcher3.LauncherAppState; 24 import com.android.launcher3.compat.AlphabeticIndexCompat; 25 import com.android.launcher3.compat.UserHandleCompat; 26 import com.android.launcher3.model.AppNameComparator; 27 import com.android.launcher3.util.ComponentKey; 28 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Locale; 34 import java.util.Map; 35 import java.util.TreeMap; 36 37 /** 38 * The alphabetically sorted list of applications. 39 */ 40 public class AlphabeticalAppsList { 41 42 public static final String TAG = "AlphabeticalAppsList"; 43 private static final boolean DEBUG = false; 44 private static final boolean DEBUG_PREDICTIONS = false; 45 46 /** 47 * Info about a section in the alphabetic list 48 */ 49 public static class SectionInfo { 50 // The number of applications in this section 51 public int numApps; 52 // The section break AdapterItem for this section 53 public AdapterItem sectionBreakItem; 54 // The first app AdapterItem for this section 55 public AdapterItem firstAppItem; 56 } 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 // The row that this item shows up on 85 public int rowIndex; 86 87 /** Section & App properties */ 88 // The section for this item 89 public SectionInfo sectionInfo; 90 91 /** App-only properties */ 92 // The section name of this app. Note that there can be multiple items with different 93 // sectionNames in the same section 94 public String sectionName = null; 95 // The index of this app in the section 96 public int sectionAppIndex = -1; 97 // The index of this app in the row 98 public int rowAppIndex; 99 // The associated AppInfo for the app 100 public AppInfo appInfo = null; 101 // The index of this app not including sections 102 public int appIndex = -1; 103 104 public static AdapterItem asSectionBreak(int pos, SectionInfo section) { 105 AdapterItem item = new AdapterItem(); 106 item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE; 107 item.position = pos; 108 item.sectionInfo = section; 109 section.sectionBreakItem = item; 110 return item; 111 } 112 113 public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName, 114 int sectionAppIndex, AppInfo appInfo, int appIndex) { 115 AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex); 116 item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE; 117 return item; 118 } 119 120 public static AdapterItem asApp(int pos, SectionInfo section, String sectionName, 121 int sectionAppIndex, AppInfo appInfo, int appIndex) { 122 AdapterItem item = new AdapterItem(); 123 item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE; 124 item.position = pos; 125 item.sectionInfo = section; 126 item.sectionName = sectionName; 127 item.sectionAppIndex = sectionAppIndex; 128 item.appInfo = appInfo; 129 item.appIndex = appIndex; 130 return item; 131 } 132 } 133 134 /** 135 * Common interface for different merging strategies. 136 */ 137 public interface MergeAlgorithm { 138 boolean continueMerging(SectionInfo section, SectionInfo withSection, 139 int sectionAppCount, int numAppsPerRow, int mergeCount); 140 } 141 142 private Launcher mLauncher; 143 144 // The set of apps from the system not including predictions 145 private final List<AppInfo> mApps = new ArrayList<>(); 146 private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>(); 147 148 // The set of filtered apps with the current filter 149 private List<AppInfo> mFilteredApps = new ArrayList<>(); 150 // The current set of adapter items 151 private List<AdapterItem> mAdapterItems = new ArrayList<>(); 152 // The set of sections for the apps with the current filter 153 private List<SectionInfo> mSections = new ArrayList<>(); 154 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 155 private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 156 // The set of predicted app component names 157 private List<ComponentKey> mPredictedAppComponents = new ArrayList<>(); 158 // The set of predicted apps resolved from the component names and the current set of apps 159 private List<AppInfo> mPredictedApps = new ArrayList<>(); 160 // The of ordered component names as a result of a search query 161 private ArrayList<ComponentKey> mSearchResults; 162 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 163 private RecyclerView.Adapter mAdapter; 164 private AlphabeticIndexCompat mIndexer; 165 private AppNameComparator mAppNameComparator; 166 private MergeAlgorithm mMergeAlgorithm; 167 private int mNumAppsPerRow; 168 private int mNumPredictedAppsPerRow; 169 private int mNumAppRowsInAdapter; 170 171 public AlphabeticalAppsList(Context context) { 172 mLauncher = (Launcher) context; 173 mIndexer = new AlphabeticIndexCompat(context); 174 mAppNameComparator = new AppNameComparator(context); 175 } 176 177 /** 178 * Sets the number of apps per row. 179 */ 180 public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, 181 MergeAlgorithm mergeAlgorithm) { 182 mNumAppsPerRow = numAppsPerRow; 183 mNumPredictedAppsPerRow = numPredictedAppsPerRow; 184 mMergeAlgorithm = mergeAlgorithm; 185 186 updateAdapterItems(); 187 } 188 189 /** 190 * Sets the adapter to notify when this dataset changes. 191 */ 192 public void setAdapter(RecyclerView.Adapter adapter) { 193 mAdapter = adapter; 194 } 195 196 /** 197 * Returns all the apps. 198 */ 199 public List<AppInfo> getApps() { 200 return mApps; 201 } 202 203 /** 204 * Returns sections of all the current filtered applications. 205 */ 206 public List<SectionInfo> getSections() { 207 return mSections; 208 } 209 210 /** 211 * Returns fast scroller sections of all the current filtered applications. 212 */ 213 public List<FastScrollSectionInfo> getFastScrollerSections() { 214 return mFastScrollerSections; 215 } 216 217 /** 218 * Returns the current filtered list of applications broken down into their sections. 219 */ 220 public List<AdapterItem> getAdapterItems() { 221 return mAdapterItems; 222 } 223 224 /** 225 * Returns the number of applications in this list. 226 */ 227 public int getSize() { 228 return mFilteredApps.size(); 229 } 230 231 /** 232 * Returns the number of rows of applications (not including predictions) 233 */ 234 public int getNumAppRows() { 235 return mNumAppRowsInAdapter; 236 } 237 238 /** 239 * Returns whether there are is a filter set. 240 */ 241 public boolean hasFilter() { 242 return (mSearchResults != null); 243 } 244 245 /** 246 * Returns whether there are no filtered results. 247 */ 248 public boolean hasNoFilteredResults() { 249 return (mSearchResults != null) && mFilteredApps.isEmpty(); 250 } 251 252 /** 253 * Sets the sorted list of filtered components. 254 */ 255 public void setOrderedFilter(ArrayList<ComponentKey> f) { 256 if (mSearchResults != f) { 257 mSearchResults = f; 258 updateAdapterItems(); 259 } 260 } 261 262 /** 263 * Sets the current set of predicted apps. Since this can be called before we get the full set 264 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 265 */ 266 public void setPredictedApps(List<ComponentKey> apps) { 267 mPredictedAppComponents.clear(); 268 mPredictedAppComponents.addAll(apps); 269 onAppsUpdated(); 270 } 271 272 /** 273 * Sets the current set of apps. 274 */ 275 public void setApps(List<AppInfo> apps) { 276 mComponentToAppMap.clear(); 277 addApps(apps); 278 } 279 280 /** 281 * Adds new apps to the list. 282 */ 283 public void addApps(List<AppInfo> apps) { 284 updateApps(apps); 285 } 286 287 /** 288 * Updates existing apps in the list 289 */ 290 public void updateApps(List<AppInfo> apps) { 291 for (AppInfo app : apps) { 292 mComponentToAppMap.put(app.toComponentKey(), app); 293 } 294 onAppsUpdated(); 295 } 296 297 /** 298 * Removes some apps from the list. 299 */ 300 public void removeApps(List<AppInfo> apps) { 301 for (AppInfo app : apps) { 302 mComponentToAppMap.remove(app.toComponentKey()); 303 } 304 onAppsUpdated(); 305 } 306 307 /** 308 * Updates internals when the set of apps are updated. 309 */ 310 private void onAppsUpdated() { 311 // Sort the list of apps 312 mApps.clear(); 313 mApps.addAll(mComponentToAppMap.values()); 314 Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); 315 316 // As a special case for some languages (currently only Simplified Chinese), we may need to 317 // coalesce sections 318 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 319 TreeMap<String, ArrayList<AppInfo>> sectionMap = null; 320 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 321 if (localeRequiresSectionSorting) { 322 // Compute the section headers. We use a TreeMap with the section name comparator to 323 // ensure that the sections are ordered when we iterate over it later 324 sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); 325 for (AppInfo info : mApps) { 326 // Add the section to the cache 327 String sectionName = getAndUpdateCachedSectionName(info.title); 328 329 // Add it to the mapping 330 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 331 if (sectionApps == null) { 332 sectionApps = new ArrayList<>(); 333 sectionMap.put(sectionName, sectionApps); 334 } 335 sectionApps.add(info); 336 } 337 338 // Add each of the section apps to the list in order 339 List<AppInfo> allApps = new ArrayList<>(mApps.size()); 340 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 341 allApps.addAll(entry.getValue()); 342 } 343 344 mApps.clear(); 345 mApps.addAll(allApps); 346 } else { 347 // Just compute the section headers for use below 348 for (AppInfo info : mApps) { 349 // Add the section to the cache 350 getAndUpdateCachedSectionName(info.title); 351 } 352 } 353 354 // Recompose the set of adapter items from the current set of apps 355 updateAdapterItems(); 356 } 357 358 /** 359 * Updates the set of filtered apps with the current filter. At this point, we expect 360 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 361 */ 362 private void updateAdapterItems() { 363 SectionInfo lastSectionInfo = null; 364 String lastSectionName = null; 365 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 366 int position = 0; 367 int appIndex = 0; 368 369 // Prepare to update the list of sections, filtered apps, etc. 370 mFilteredApps.clear(); 371 mFastScrollerSections.clear(); 372 mAdapterItems.clear(); 373 mSections.clear(); 374 375 if (DEBUG_PREDICTIONS) { 376 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { 377 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 378 UserHandleCompat.myUserHandle())); 379 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 380 UserHandleCompat.myUserHandle())); 381 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 382 UserHandleCompat.myUserHandle())); 383 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 384 UserHandleCompat.myUserHandle())); 385 } 386 } 387 388 // Process the predicted app components 389 mPredictedApps.clear(); 390 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 391 for (ComponentKey ck : mPredictedAppComponents) { 392 AppInfo info = mComponentToAppMap.get(ck); 393 if (info != null) { 394 mPredictedApps.add(info); 395 } else { 396 if (LauncherAppState.isDogfoodBuild()) { 397 Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); 398 } 399 } 400 // Stop at the number of predicted apps 401 if (mPredictedApps.size() == mNumPredictedAppsPerRow) { 402 break; 403 } 404 } 405 406 if (!mPredictedApps.isEmpty()) { 407 // Add a section for the predictions 408 lastSectionInfo = new SectionInfo(); 409 lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); 410 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 411 mSections.add(lastSectionInfo); 412 mFastScrollerSections.add(lastFastScrollerSectionInfo); 413 mAdapterItems.add(sectionItem); 414 415 // Add the predicted app items 416 for (AppInfo info : mPredictedApps) { 417 AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, 418 "", lastSectionInfo.numApps++, info, appIndex++); 419 if (lastSectionInfo.firstAppItem == null) { 420 lastSectionInfo.firstAppItem = appItem; 421 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 422 } 423 mAdapterItems.add(appItem); 424 mFilteredApps.add(info); 425 } 426 } 427 } 428 429 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 430 // ordered set of sections 431 for (AppInfo info : getFiltersAppInfos()) { 432 String sectionName = getAndUpdateCachedSectionName(info.title); 433 434 // Create a new section if the section names do not match 435 if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { 436 lastSectionName = sectionName; 437 lastSectionInfo = new SectionInfo(); 438 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 439 mSections.add(lastSectionInfo); 440 mFastScrollerSections.add(lastFastScrollerSectionInfo); 441 442 // Create a new section item to break the flow of items in the list 443 if (!hasFilter()) { 444 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 445 mAdapterItems.add(sectionItem); 446 } 447 } 448 449 // Create an app item 450 AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, 451 lastSectionInfo.numApps++, info, appIndex++); 452 if (lastSectionInfo.firstAppItem == null) { 453 lastSectionInfo.firstAppItem = appItem; 454 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 455 } 456 mAdapterItems.add(appItem); 457 mFilteredApps.add(info); 458 } 459 460 // Merge multiple sections together as requested by the merge strategy for this device 461 mergeSections(); 462 463 if (mNumAppsPerRow != 0) { 464 // Update the number of rows in the adapter after we do all the merging (otherwise, we 465 // would have to shift the values again) 466 int numAppsInSection = 0; 467 int numAppsInRow = 0; 468 int rowIndex = -1; 469 for (AdapterItem item : mAdapterItems) { 470 item.rowIndex = 0; 471 if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) { 472 numAppsInSection = 0; 473 } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || 474 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 475 if (numAppsInSection % mNumAppsPerRow == 0) { 476 numAppsInRow = 0; 477 rowIndex++; 478 } 479 item.rowIndex = rowIndex; 480 item.rowAppIndex = numAppsInRow; 481 numAppsInSection++; 482 numAppsInRow++; 483 } 484 } 485 mNumAppRowsInAdapter = rowIndex + 1; 486 487 // Pre-calculate all the fast scroller fractions based on the number of rows 488 float rowFraction = 1f / mNumAppRowsInAdapter; 489 for (FastScrollSectionInfo info : mFastScrollerSections) { 490 AdapterItem item = info.fastScrollToItem; 491 if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && 492 item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 493 info.touchFraction = 0f; 494 continue; 495 } 496 497 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 498 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 499 } 500 } 501 502 // Refresh the recycler view 503 if (mAdapter != null) { 504 mAdapter.notifyDataSetChanged(); 505 } 506 } 507 508 private List<AppInfo> getFiltersAppInfos() { 509 if (mSearchResults == null) { 510 return mApps; 511 } 512 513 ArrayList<AppInfo> result = new ArrayList<>(); 514 for (ComponentKey key : mSearchResults) { 515 AppInfo match = mComponentToAppMap.get(key); 516 if (match != null) { 517 result.add(match); 518 } 519 } 520 return result; 521 } 522 523 /** 524 * Merges multiple sections to reduce visual raggedness. 525 */ 526 private void mergeSections() { 527 // Ignore merging until we have an algorithm and a valid row size 528 if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { 529 return; 530 } 531 532 // Go through each section and try and merge some of the sections 533 if (!hasFilter()) { 534 int sectionAppCount = 0; 535 for (int i = 0; i < mSections.size() - 1; i++) { 536 SectionInfo section = mSections.get(i); 537 sectionAppCount = section.numApps; 538 int mergeCount = 1; 539 540 // Merge rows based on the current strategy 541 while (i < (mSections.size() - 1) && 542 mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), 543 sectionAppCount, mNumAppsPerRow, mergeCount)) { 544 SectionInfo nextSection = mSections.remove(i + 1); 545 546 // Remove the next section break 547 mAdapterItems.remove(nextSection.sectionBreakItem); 548 int pos = mAdapterItems.indexOf(section.firstAppItem); 549 550 // Point the section for these new apps to the merged section 551 int nextPos = pos + section.numApps; 552 for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { 553 AdapterItem item = mAdapterItems.get(j); 554 item.sectionInfo = section; 555 item.sectionAppIndex += section.numApps; 556 } 557 558 // Update the following adapter items of the removed section item 559 pos = mAdapterItems.indexOf(nextSection.firstAppItem); 560 for (int j = pos; j < mAdapterItems.size(); j++) { 561 AdapterItem item = mAdapterItems.get(j); 562 item.position--; 563 } 564 section.numApps += nextSection.numApps; 565 sectionAppCount += nextSection.numApps; 566 567 if (DEBUG) { 568 Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + 569 " to " + section.firstAppItem.sectionName + 570 " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); 571 } 572 mergeCount++; 573 } 574 } 575 } 576 } 577 578 /** 579 * Returns the cached section name for the given title, recomputing and updating the cache if 580 * the title has no cached section name. 581 */ 582 private String getAndUpdateCachedSectionName(CharSequence title) { 583 String sectionName = mCachedSectionNames.get(title); 584 if (sectionName == null) { 585 sectionName = mIndexer.computeSectionName(title); 586 mCachedSectionNames.put(title, sectionName); 587 } 588 return sectionName; 589 } 590 } 591