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