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