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 278 /** 279 * Sets the sorted list of filtered components. 280 */ setOrderedFilter(ArrayList<ComponentKey> f)281 public void setOrderedFilter(ArrayList<ComponentKey> f) { 282 if (mSearchResults != f) { 283 mSearchResults = f; 284 updateAdapterItems(); 285 } 286 } 287 288 /** 289 * Sets the current set of predicted apps. Since this can be called before we get the full set 290 * of applications, we should merge the results only in onAppsUpdated() which is idempotent. 291 */ setPredictedApps(List<ComponentKey> apps)292 public void setPredictedApps(List<ComponentKey> apps) { 293 mPredictedAppComponents.clear(); 294 mPredictedAppComponents.addAll(apps); 295 onAppsUpdated(); 296 } 297 298 /** 299 * Sets the current set of apps. 300 */ setApps(List<AppInfo> apps)301 public void setApps(List<AppInfo> apps) { 302 mComponentToAppMap.clear(); 303 addApps(apps); 304 } 305 306 /** 307 * Adds new apps to the list. 308 */ addApps(List<AppInfo> apps)309 public void addApps(List<AppInfo> apps) { 310 updateApps(apps); 311 } 312 313 /** 314 * Updates existing apps in the list 315 */ updateApps(List<AppInfo> apps)316 public void updateApps(List<AppInfo> apps) { 317 for (AppInfo app : apps) { 318 mComponentToAppMap.put(app.toComponentKey(), app); 319 } 320 onAppsUpdated(); 321 } 322 323 /** 324 * Removes some apps from the list. 325 */ removeApps(List<AppInfo> apps)326 public void removeApps(List<AppInfo> apps) { 327 for (AppInfo app : apps) { 328 mComponentToAppMap.remove(app.toComponentKey()); 329 } 330 onAppsUpdated(); 331 } 332 333 /** 334 * Updates internals when the set of apps are updated. 335 */ onAppsUpdated()336 private void onAppsUpdated() { 337 // Sort the list of apps 338 mApps.clear(); 339 mApps.addAll(mComponentToAppMap.values()); 340 Collections.sort(mApps, mAppNameComparator.getAppInfoComparator()); 341 342 // As a special case for some languages (currently only Simplified Chinese), we may need to 343 // coalesce sections 344 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 345 TreeMap<String, ArrayList<AppInfo>> sectionMap = null; 346 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 347 if (localeRequiresSectionSorting) { 348 // Compute the section headers. We use a TreeMap with the section name comparator to 349 // ensure that the sections are ordered when we iterate over it later 350 sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator()); 351 for (AppInfo info : mApps) { 352 // Add the section to the cache 353 String sectionName = getAndUpdateCachedSectionName(info.title); 354 355 // Add it to the mapping 356 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 357 if (sectionApps == null) { 358 sectionApps = new ArrayList<>(); 359 sectionMap.put(sectionName, sectionApps); 360 } 361 sectionApps.add(info); 362 } 363 364 // Add each of the section apps to the list in order 365 List<AppInfo> allApps = new ArrayList<>(mApps.size()); 366 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 367 allApps.addAll(entry.getValue()); 368 } 369 370 mApps.clear(); 371 mApps.addAll(allApps); 372 } else { 373 // Just compute the section headers for use below 374 for (AppInfo info : mApps) { 375 // Add the section to the cache 376 getAndUpdateCachedSectionName(info.title); 377 } 378 } 379 380 // Recompose the set of adapter items from the current set of apps 381 updateAdapterItems(); 382 } 383 384 /** 385 * Updates the set of filtered apps with the current filter. At this point, we expect 386 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 387 */ updateAdapterItems()388 private void updateAdapterItems() { 389 SectionInfo lastSectionInfo = null; 390 String lastSectionName = null; 391 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 392 int position = 0; 393 int appIndex = 0; 394 395 // Prepare to update the list of sections, filtered apps, etc. 396 mFilteredApps.clear(); 397 mFastScrollerSections.clear(); 398 mAdapterItems.clear(); 399 mSections.clear(); 400 401 if (DEBUG_PREDICTIONS) { 402 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) { 403 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 404 UserHandleCompat.myUserHandle())); 405 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 406 UserHandleCompat.myUserHandle())); 407 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 408 UserHandleCompat.myUserHandle())); 409 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName, 410 UserHandleCompat.myUserHandle())); 411 } 412 } 413 414 // Process the predicted app components 415 mPredictedApps.clear(); 416 if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) { 417 for (ComponentKey ck : mPredictedAppComponents) { 418 AppInfo info = mComponentToAppMap.get(ck); 419 if (info != null) { 420 mPredictedApps.add(info); 421 } else { 422 if (LauncherAppState.isDogfoodBuild()) { 423 Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher)); 424 } 425 } 426 // Stop at the number of predicted apps 427 if (mPredictedApps.size() == mNumPredictedAppsPerRow) { 428 break; 429 } 430 } 431 432 if (!mPredictedApps.isEmpty()) { 433 // Add a section for the predictions 434 lastSectionInfo = new SectionInfo(); 435 lastFastScrollerSectionInfo = new FastScrollSectionInfo(""); 436 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 437 mSections.add(lastSectionInfo); 438 mFastScrollerSections.add(lastFastScrollerSectionInfo); 439 mAdapterItems.add(sectionItem); 440 441 // Add the predicted app items 442 for (AppInfo info : mPredictedApps) { 443 AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo, 444 "", lastSectionInfo.numApps++, info, appIndex++); 445 if (lastSectionInfo.firstAppItem == null) { 446 lastSectionInfo.firstAppItem = appItem; 447 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 448 } 449 mAdapterItems.add(appItem); 450 mFilteredApps.add(info); 451 } 452 } 453 } 454 455 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 456 // ordered set of sections 457 for (AppInfo info : getFiltersAppInfos()) { 458 String sectionName = getAndUpdateCachedSectionName(info.title); 459 460 // Create a new section if the section names do not match 461 if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) { 462 lastSectionName = sectionName; 463 lastSectionInfo = new SectionInfo(); 464 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 465 mSections.add(lastSectionInfo); 466 mFastScrollerSections.add(lastFastScrollerSectionInfo); 467 468 // Create a new section item to break the flow of items in the list 469 if (!hasFilter()) { 470 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo); 471 mAdapterItems.add(sectionItem); 472 } 473 } 474 475 // Create an app item 476 AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName, 477 lastSectionInfo.numApps++, info, appIndex++); 478 if (lastSectionInfo.firstAppItem == null) { 479 lastSectionInfo.firstAppItem = appItem; 480 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 481 } 482 mAdapterItems.add(appItem); 483 mFilteredApps.add(info); 484 } 485 486 // Append the search market item if we are currently searching 487 if (hasFilter()) { 488 if (hasNoFilteredResults()) { 489 mAdapterItems.add(AdapterItem.asEmptySearch(position++)); 490 } else { 491 mAdapterItems.add(AdapterItem.asDivider(position++)); 492 } 493 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 494 } 495 496 // Merge multiple sections together as requested by the merge strategy for this device 497 mergeSections(); 498 499 if (mNumAppsPerRow != 0) { 500 // Update the number of rows in the adapter after we do all the merging (otherwise, we 501 // would have to shift the values again) 502 int numAppsInSection = 0; 503 int numAppsInRow = 0; 504 int rowIndex = -1; 505 for (AdapterItem item : mAdapterItems) { 506 item.rowIndex = 0; 507 if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) { 508 numAppsInSection = 0; 509 } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE || 510 item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 511 if (numAppsInSection % mNumAppsPerRow == 0) { 512 numAppsInRow = 0; 513 rowIndex++; 514 } 515 item.rowIndex = rowIndex; 516 item.rowAppIndex = numAppsInRow; 517 numAppsInSection++; 518 numAppsInRow++; 519 } 520 } 521 mNumAppRowsInAdapter = rowIndex + 1; 522 523 // Pre-calculate all the fast scroller fractions 524 switch (mFastScrollDistributionMode) { 525 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: 526 float rowFraction = 1f / mNumAppRowsInAdapter; 527 for (FastScrollSectionInfo info : mFastScrollerSections) { 528 AdapterItem item = info.fastScrollToItem; 529 if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && 530 item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 531 info.touchFraction = 0f; 532 continue; 533 } 534 535 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 536 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 537 } 538 break; 539 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: 540 float perSectionTouchFraction = 1f / mFastScrollerSections.size(); 541 float cumulativeTouchFraction = 0f; 542 for (FastScrollSectionInfo info : mFastScrollerSections) { 543 AdapterItem item = info.fastScrollToItem; 544 if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE && 545 item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) { 546 info.touchFraction = 0f; 547 continue; 548 } 549 info.touchFraction = cumulativeTouchFraction; 550 cumulativeTouchFraction += perSectionTouchFraction; 551 } 552 break; 553 } 554 } 555 556 // Refresh the recycler view 557 if (mAdapter != null) { 558 mAdapter.notifyDataSetChanged(); 559 } 560 } 561 getFiltersAppInfos()562 private List<AppInfo> getFiltersAppInfos() { 563 if (mSearchResults == null) { 564 return mApps; 565 } 566 567 ArrayList<AppInfo> result = new ArrayList<>(); 568 for (ComponentKey key : mSearchResults) { 569 AppInfo match = mComponentToAppMap.get(key); 570 if (match != null) { 571 result.add(match); 572 } 573 } 574 return result; 575 } 576 577 /** 578 * Merges multiple sections to reduce visual raggedness. 579 */ mergeSections()580 private void mergeSections() { 581 // Ignore merging until we have an algorithm and a valid row size 582 if (mMergeAlgorithm == null || mNumAppsPerRow == 0) { 583 return; 584 } 585 586 // Go through each section and try and merge some of the sections 587 if (!hasFilter()) { 588 int sectionAppCount = 0; 589 for (int i = 0; i < mSections.size() - 1; i++) { 590 SectionInfo section = mSections.get(i); 591 sectionAppCount = section.numApps; 592 int mergeCount = 1; 593 594 // Merge rows based on the current strategy 595 while (i < (mSections.size() - 1) && 596 mMergeAlgorithm.continueMerging(section, mSections.get(i + 1), 597 sectionAppCount, mNumAppsPerRow, mergeCount)) { 598 SectionInfo nextSection = mSections.remove(i + 1); 599 600 // Remove the next section break 601 mAdapterItems.remove(nextSection.sectionBreakItem); 602 int pos = mAdapterItems.indexOf(section.firstAppItem); 603 604 // Point the section for these new apps to the merged section 605 int nextPos = pos + section.numApps; 606 for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) { 607 AdapterItem item = mAdapterItems.get(j); 608 item.sectionInfo = section; 609 item.sectionAppIndex += section.numApps; 610 } 611 612 // Update the following adapter items of the removed section item 613 pos = mAdapterItems.indexOf(nextSection.firstAppItem); 614 for (int j = pos; j < mAdapterItems.size(); j++) { 615 AdapterItem item = mAdapterItems.get(j); 616 item.position--; 617 } 618 section.numApps += nextSection.numApps; 619 sectionAppCount += nextSection.numApps; 620 621 if (DEBUG) { 622 Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName + 623 " to " + section.firstAppItem.sectionName + 624 " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow)); 625 } 626 mergeCount++; 627 } 628 } 629 } 630 } 631 632 /** 633 * Returns the cached section name for the given title, recomputing and updating the cache if 634 * the title has no cached section name. 635 */ getAndUpdateCachedSectionName(CharSequence title)636 private String getAndUpdateCachedSectionName(CharSequence title) { 637 String sectionName = mCachedSectionNames.get(title); 638 if (sectionName == null) { 639 sectionName = mIndexer.computeSectionName(title); 640 mCachedSectionNames.put(title, sectionName); 641 } 642 return sectionName; 643 } 644 } 645