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