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