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