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