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