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