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