• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
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.VIEW_TYPE_SECTION_BREAK;
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.VIEW_TYPE_PREDICTION_ICON;
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.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 
asEmptySearch(int pos)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 
asPredictionDivider(int pos)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 
asSearchDivder(int pos)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 
asMarketDivider(int pos)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 
asMarketSearch(int pos)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 {
continueMerging(SectionInfo section, SectionInfo withSection, int sectionAppCount, int numAppsPerRow, int mergeCount)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 
AlphabeticalAppsList(Context context)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      */
setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow, MergeAlgorithm mergeAlgorithm)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      */
setAdapter(AllAppsGridAdapter adapter)232     public void setAdapter(AllAppsGridAdapter adapter) {
233         mAdapter = adapter;
234     }
235 
236     /**
237      * Returns all the apps.
238      */
getApps()239     public List<AppInfo> getApps() {
240         return mApps;
241     }
242 
243     /**
244      * Returns sections of all the current filtered applications.
245      */
getSections()246     public List<SectionInfo> getSections() {
247         return mSections;
248     }
249 
250     /**
251      * Returns fast scroller sections of all the current filtered applications.
252      */
getFastScrollerSections()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      */
getAdapterItems()260     public List<AdapterItem> getAdapterItems() {
261         return mAdapterItems;
262     }
263 
264     /**
265      * Returns the number of rows of applications (not including predictions)
266      */
getNumAppRows()267     public int getNumAppRows() {
268         return mNumAppRowsInAdapter;
269     }
270 
271     /**
272      * Returns the number of applications in this list.
273      */
getNumFilteredApps()274     public int getNumFilteredApps() {
275         return mFilteredApps.size();
276     }
277 
278     /**
279      * Returns whether there are is a filter set.
280      */
hasFilter()281     public boolean hasFilter() {
282         return (mSearchResults != null);
283     }
284 
285     /**
286      * Returns whether there are no filtered results.
287      */
hasNoFilteredResults()288     public boolean hasNoFilteredResults() {
289         return (mSearchResults != null) && mFilteredApps.isEmpty();
290     }
291 
292     /**
293      * Sets the sorted list of filtered components.
294      */
setOrderedFilter(ArrayList<ComponentKey> f)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      */
setPredictedApps(List<ComponentKey> apps)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      */
setApps(List<AppInfo> apps)318     public void setApps(List<AppInfo> apps) {
319         mComponentToAppMap.clear();
320         addApps(apps);
321     }
322 
323     /**
324      * Adds new apps to the list.
325      */
addApps(List<AppInfo> apps)326     public void addApps(List<AppInfo> apps) {
327         updateApps(apps);
328     }
329 
330     /**
331      * Updates existing apps in the list
332      */
updateApps(List<AppInfo> apps)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      */
removeApps(List<AppInfo> apps)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      */
onAppsUpdated()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      */
updateAdapterItems()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 
getFiltersAppInfos()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      */
mergeSections()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      */
getAndUpdateCachedSectionName(CharSequence title)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