AlphabeticalAppsList.java revision c0372a42a256421199e5d41592b69a6f4c2e9ef1
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.os.Process;
20import android.support.annotation.NonNull;
21import android.support.annotation.Nullable;
22import android.util.Log;
23
24import com.android.launcher3.AppInfo;
25import com.android.launcher3.Launcher;
26import com.android.launcher3.compat.AlphabeticIndexCompat;
27import com.android.launcher3.config.FeatureFlags;
28import com.android.launcher3.discovery.AppDiscoveryAppInfo;
29import com.android.launcher3.discovery.AppDiscoveryItem;
30import com.android.launcher3.discovery.AppDiscoveryUpdateState;
31import com.android.launcher3.util.ComponentKey;
32import com.android.launcher3.util.LabelComparator;
33
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    private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0;
52    private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1;
53
54    private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
55
56    private AppDiscoveryUpdateState mAppDiscoveryUpdateState;
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        /** App-only properties */
86        // The section name of this app.  Note that there can be multiple items with different
87        // sectionNames in the same section
88        public String sectionName = null;
89        // The row that this item shows up on
90        public int rowIndex;
91        // The index of this app in the row
92        public int rowAppIndex;
93        // The associated AppInfo for the app
94        public AppInfo appInfo = null;
95        // The index of this app not including sections
96        public int appIndex = -1;
97
98        public static AdapterItem asPredictedApp(int pos, String sectionName, AppInfo appInfo,
99                int appIndex) {
100            AdapterItem item = asApp(pos, sectionName, appInfo, appIndex);
101            item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON;
102            return item;
103        }
104
105        public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
106                int appIndex) {
107            AdapterItem item = new AdapterItem();
108            item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON;
109            item.position = pos;
110            item.sectionName = sectionName;
111            item.appInfo = appInfo;
112            item.appIndex = appIndex;
113            return item;
114        }
115
116        public static AdapterItem asDiscoveryItem(int pos, String sectionName, AppInfo appInfo,
117                int appIndex) {
118            AdapterItem item = new AdapterItem();
119            item.viewType = AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM;
120            item.position = pos;
121            item.sectionName = sectionName;
122            item.appInfo = appInfo;
123            item.appIndex = appIndex;
124            return item;
125        }
126
127        public static AdapterItem asEmptySearch(int pos) {
128            AdapterItem item = new AdapterItem();
129            item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH;
130            item.position = pos;
131            return item;
132        }
133
134        public static AdapterItem asPredictionDivider(int pos) {
135            AdapterItem item = new AdapterItem();
136            item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER;
137            item.position = pos;
138            return item;
139        }
140
141        public static AdapterItem asSearchDivider(int pos) {
142            AdapterItem item = new AdapterItem();
143            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER;
144            item.position = pos;
145            return item;
146        }
147
148        public static AdapterItem asMarketDivider(int pos) {
149            AdapterItem item = new AdapterItem();
150            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER;
151            item.position = pos;
152            return item;
153        }
154
155        public static AdapterItem asLoadingDivider(int pos) {
156            AdapterItem item = new AdapterItem();
157            item.viewType = AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER;
158            item.position = pos;
159            return item;
160        }
161
162        public static AdapterItem asMarketSearch(int pos) {
163            AdapterItem item = new AdapterItem();
164            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET;
165            item.position = pos;
166            return item;
167        }
168    }
169
170    private final Launcher mLauncher;
171
172    // The set of apps from the system not including predictions
173    private final List<AppInfo> mApps = new ArrayList<>();
174    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
175
176    // The set of filtered apps with the current filter
177    private final List<AppInfo> mFilteredApps = new ArrayList<>();
178    // The current set of adapter items
179    private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
180    // The set of sections that we allow fast-scrolling to (includes non-merged sections)
181    private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
182    // The set of predicted app component names
183    private final List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
184    // The set of predicted apps resolved from the component names and the current set of apps
185    private final List<AppInfo> mPredictedApps = new ArrayList<>();
186    // The set of apps returned from a discovery service while searching
187    private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>();
188    // The suggested app returned by a discovery service
189    private AppDiscoveryAppInfo mSuggestedApp;
190
191    // The of ordered component names as a result of a search query
192    private ArrayList<ComponentKey> mSearchResults;
193    private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
194    private AllAppsGridAdapter mAdapter;
195    private AlphabeticIndexCompat mIndexer;
196    private AppInfoComparator mAppNameComparator;
197    private int mNumAppsPerRow;
198    private int mNumPredictedAppsPerRow;
199    private int mNumAppRowsInAdapter;
200
201    public AlphabeticalAppsList(Context context) {
202        mLauncher = Launcher.getLauncher(context);
203        mIndexer = new AlphabeticIndexCompat(context);
204        mAppNameComparator = new AppInfoComparator(context);
205    }
206
207    /**
208     * Sets the number of apps per row.
209     */
210    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
211        mNumAppsPerRow = numAppsPerRow;
212        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
213
214        updateAdapterItems();
215    }
216
217    /**
218     * Sets the adapter to notify when this dataset changes.
219     */
220    public void setAdapter(AllAppsGridAdapter adapter) {
221        mAdapter = adapter;
222    }
223
224    /**
225     * Returns all the apps.
226     */
227    public List<AppInfo> getApps() {
228        return mApps;
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    boolean shouldShowEmptySearch() {
274        return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty();
275    }
276
277    /**
278     * Sets the sorted list of filtered components.
279     */
280    public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
281        if (mSearchResults != f) {
282            boolean same = mSearchResults != null && mSearchResults.equals(f);
283            mSearchResults = f;
284            updateAdapterItems();
285            return !same;
286        }
287        return false;
288    }
289
290    public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
291                @NonNull AppDiscoveryUpdateState state) {
292        mAppDiscoveryUpdateState = state;
293        switch (state) {
294            case SUGGESTED:
295                mSuggestedApp = app != null ? new AppDiscoveryAppInfo(app) : null;
296                break;
297            case START:
298                mDiscoveredApps.clear();
299                break;
300            case UPDATE:
301                mDiscoveredApps.add(new AppDiscoveryAppInfo(app));
302                break;
303        }
304        updateAdapterItems();
305    }
306
307    /**
308     * Sets the current set of predicted apps.  Since this can be called before we get the full set
309     * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
310     */
311    public void setPredictedApps(List<ComponentKey> apps) {
312        mPredictedAppComponents.clear();
313        mPredictedAppComponents.addAll(apps);
314        onAppsUpdated();
315    }
316
317    /**
318     * Sets the current set of apps.
319     */
320    public void setApps(List<AppInfo> apps) {
321        mComponentToAppMap.clear();
322        addApps(apps);
323    }
324
325    /**
326     * Adds new apps to the list.
327     */
328    public void addApps(List<AppInfo> apps) {
329        updateApps(apps);
330    }
331
332    /**
333     * Updates existing apps in the list
334     */
335    public void updateApps(List<AppInfo> apps) {
336        for (AppInfo app : apps) {
337            mComponentToAppMap.put(app.toComponentKey(), app);
338        }
339        onAppsUpdated();
340    }
341
342    /**
343     * Removes some apps from the list.
344     */
345    public void removeApps(List<AppInfo> apps) {
346        for (AppInfo app : apps) {
347            mComponentToAppMap.remove(app.toComponentKey());
348        }
349        onAppsUpdated();
350    }
351
352    /**
353     * Updates internals when the set of apps are updated.
354     */
355    private void onAppsUpdated() {
356        // Sort the list of apps
357        mApps.clear();
358        mApps.addAll(mComponentToAppMap.values());
359        Collections.sort(mApps, mAppNameComparator);
360
361        // As a special case for some languages (currently only Simplified Chinese), we may need to
362        // coalesce sections
363        Locale curLocale = mLauncher.getResources().getConfiguration().locale;
364        boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
365        if (localeRequiresSectionSorting) {
366            // Compute the section headers. We use a TreeMap with the section name comparator to
367            // ensure that the sections are ordered when we iterate over it later
368            TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator());
369            for (AppInfo info : mApps) {
370                // Add the section to the cache
371                String sectionName = getAndUpdateCachedSectionName(info.title);
372
373                // Add it to the mapping
374                ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
375                if (sectionApps == null) {
376                    sectionApps = new ArrayList<>();
377                    sectionMap.put(sectionName, sectionApps);
378                }
379                sectionApps.add(info);
380            }
381
382            // Add each of the section apps to the list in order
383            mApps.clear();
384            for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
385                mApps.addAll(entry.getValue());
386            }
387        } else {
388            // Just compute the section headers for use below
389            for (AppInfo info : mApps) {
390                // Add the section to the cache
391                getAndUpdateCachedSectionName(info.title);
392            }
393        }
394
395        // Recompose the set of adapter items from the current set of apps
396        updateAdapterItems();
397    }
398
399    /**
400     * Updates the set of filtered apps with the current filter.  At this point, we expect
401     * mCachedSectionNames to have been calculated for the set of all apps in mApps.
402     */
403    private void updateAdapterItems() {
404        refillAdapterItems();
405        refreshRecyclerView();
406    }
407
408    private void refreshRecyclerView() {
409        if (mAdapter != null) {
410            mAdapter.notifyDataSetChanged();
411        }
412    }
413
414    private void refillAdapterItems() {
415        String lastSectionName = null;
416        FastScrollSectionInfo lastFastScrollerSectionInfo = null;
417        int position = 0;
418        int appIndex = 0;
419
420        // Prepare to update the list of sections, filtered apps, etc.
421        mFilteredApps.clear();
422        mFastScrollerSections.clear();
423        mAdapterItems.clear();
424
425        if (DEBUG_PREDICTIONS) {
426            if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
427                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
428                        Process.myUserHandle()));
429                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
430                        Process.myUserHandle()));
431                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
432                        Process.myUserHandle()));
433                mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
434                        Process.myUserHandle()));
435            }
436        }
437
438        // Add the search divider
439        mAdapterItems.add(AdapterItem.asSearchDivider(position++));
440
441        // Process the predicted app components
442        mPredictedApps.clear();
443        if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
444            int suggestedAppsCount = mSuggestedApp != null ? 1 : 0;
445            for (ComponentKey ck : mPredictedAppComponents) {
446                AppInfo info = mComponentToAppMap.get(ck);
447                if (info != null) {
448                    mPredictedApps.add(info);
449                } else {
450                    if (FeatureFlags.IS_DOGFOOD_BUILD) {
451                        Log.e(TAG, "Predicted app not found: " + ck);
452                    }
453                }
454                // Stop at the number of predicted apps
455                if (mPredictedApps.size() + suggestedAppsCount == mNumPredictedAppsPerRow) {
456                    break;
457                }
458            }
459
460            if (mSuggestedApp != null) {
461                // adding suggested app as the last predicted app
462                mPredictedApps.add(mSuggestedApp);
463            }
464
465            if (!mPredictedApps.isEmpty()) {
466                // Add a section for the predictions
467                lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
468                mFastScrollerSections.add(lastFastScrollerSectionInfo);
469
470                // Add the predicted app items
471                for (AppInfo info : mPredictedApps) {
472                    AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info,
473                            appIndex++);
474                    if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
475                        lastFastScrollerSectionInfo.fastScrollToItem = appItem;
476                    }
477                    mAdapterItems.add(appItem);
478                    mFilteredApps.add(info);
479                }
480
481                mAdapterItems.add(AdapterItem.asPredictionDivider(position++));
482            }
483        }
484
485        // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
486        // ordered set of sections
487        for (AppInfo info : getFiltersAppInfos()) {
488            String sectionName = getAndUpdateCachedSectionName(info.title);
489
490            // Create a new section if the section names do not match
491            if (!sectionName.equals(lastSectionName)) {
492                lastSectionName = sectionName;
493                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
494                mFastScrollerSections.add(lastFastScrollerSectionInfo);
495            }
496
497            // Create an app item
498            AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
499            if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
500                lastFastScrollerSectionInfo.fastScrollToItem = appItem;
501            }
502            mAdapterItems.add(appItem);
503            mFilteredApps.add(info);
504        }
505
506        if (hasFilter()) {
507            if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) {
508                mAdapterItems.add(AdapterItem.asLoadingDivider(position++));
509                // Append all app discovery results
510                for (int i = 0; i < mDiscoveredApps.size(); i++) {
511                    AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i);
512                    if (appDiscoveryAppInfo.isRecent) {
513                        // already handled in getFilteredAppInfos()
514                        continue;
515                    }
516                    AdapterItem item = AdapterItem.asDiscoveryItem(position++,
517                            "", appDiscoveryAppInfo, appIndex++);
518                    mAdapterItems.add(item);
519                }
520
521                if (!isAppDiscoveryRunning()) {
522                    mAdapterItems.add(AdapterItem.asMarketSearch(position++));
523                }
524            } else {
525                // Append the search market item
526                if (hasNoFilteredResults()) {
527                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
528                } else {
529                    mAdapterItems.add(AdapterItem.asMarketDivider(position++));
530                }
531                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
532            }
533        }
534
535        if (mNumAppsPerRow != 0) {
536            // Update the number of rows in the adapter after we do all the merging (otherwise, we
537            // would have to shift the values again)
538            int numAppsInSection = 0;
539            int numAppsInRow = 0;
540            int rowIndex = -1;
541            for (AdapterItem item : mAdapterItems) {
542                item.rowIndex = 0;
543                if (AllAppsGridAdapter.isDividerViewType(item.viewType)) {
544                    numAppsInSection = 0;
545                } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
546                    if (numAppsInSection % mNumAppsPerRow == 0) {
547                        numAppsInRow = 0;
548                        rowIndex++;
549                    }
550                    item.rowIndex = rowIndex;
551                    item.rowAppIndex = numAppsInRow;
552                    numAppsInSection++;
553                    numAppsInRow++;
554                }
555            }
556            mNumAppRowsInAdapter = rowIndex + 1;
557
558            // Pre-calculate all the fast scroller fractions
559            switch (mFastScrollDistributionMode) {
560                case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION:
561                    float rowFraction = 1f / mNumAppRowsInAdapter;
562                    for (FastScrollSectionInfo info : mFastScrollerSections) {
563                        AdapterItem item = info.fastScrollToItem;
564                        if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
565                            info.touchFraction = 0f;
566                            continue;
567                        }
568
569                        float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
570                        info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
571                    }
572                    break;
573                case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS:
574                    float perSectionTouchFraction = 1f / mFastScrollerSections.size();
575                    float cumulativeTouchFraction = 0f;
576                    for (FastScrollSectionInfo info : mFastScrollerSections) {
577                        AdapterItem item = info.fastScrollToItem;
578                        if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
579                            info.touchFraction = 0f;
580                            continue;
581                        }
582                        info.touchFraction = cumulativeTouchFraction;
583                        cumulativeTouchFraction += perSectionTouchFraction;
584                    }
585                    break;
586            }
587        }
588    }
589
590    public boolean isAppDiscoveryRunning() {
591        return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START
592                || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE;
593    }
594
595    private List<AppInfo> getFiltersAppInfos() {
596        if (mSearchResults == null) {
597            return mApps;
598        }
599
600        ArrayList<AppInfo> result = new ArrayList<>();
601        for (ComponentKey key : mSearchResults) {
602            AppInfo match = mComponentToAppMap.get(key);
603            if (match != null) {
604                result.add(match);
605            }
606        }
607
608        // adding recently used instant apps
609        if (mDiscoveredApps.size() > 0) {
610            for (int i = 0; i < mDiscoveredApps.size(); i++) {
611                AppDiscoveryAppInfo discoveryAppInfo = mDiscoveredApps.get(i);
612                if (discoveryAppInfo.isRecent) {
613                    result.add(discoveryAppInfo);
614                }
615            }
616            Collections.sort(result, mAppNameComparator);
617        }
618        return result;
619    }
620
621    /**
622     * Returns the cached section name for the given title, recomputing and updating the cache if
623     * the title has no cached section name.
624     */
625    private String getAndUpdateCachedSectionName(CharSequence title) {
626        String sectionName = mCachedSectionNames.get(title);
627        if (sectionName == null) {
628            sectionName = mIndexer.computeSectionName(title);
629            mCachedSectionNames.put(title, sectionName);
630        }
631        return sectionName;
632    }
633
634}
635