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.ComponentKeyMapper;
33import com.android.launcher3.util.LabelComparator;
34
35import java.util.ArrayList;
36import java.util.Collections;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Locale;
40import java.util.Map;
41import java.util.TreeMap;
42
43/**
44 * The alphabetically sorted list of applications.
45 */
46public class AlphabeticalAppsList {
47
48    public static final String TAG = "AlphabeticalAppsList";
49    private static final boolean DEBUG = false;
50    private static final boolean DEBUG_PREDICTIONS = false;
51
52    private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0;
53    private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1;
54
55    private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
56
57    private AppDiscoveryUpdateState mAppDiscoveryUpdateState;
58
59    /**
60     * Info about a fast scroller section, depending if sections are merged, the fast scroller
61     * sections will not be the same set as the section headers.
62     */
63    public static class FastScrollSectionInfo {
64        // The section name
65        public String sectionName;
66        // The AdapterItem to scroll to for this section
67        public AdapterItem fastScrollToItem;
68        // The touch fraction that should map to this fast scroll section info
69        public float touchFraction;
70
71        public FastScrollSectionInfo(String sectionName) {
72            this.sectionName = sectionName;
73        }
74    }
75
76    /**
77     * Info about a particular adapter item (can be either section or app)
78     */
79    public static class AdapterItem {
80        /** Common properties */
81        // The index of this adapter item in the list
82        public int position;
83        // The type of this item
84        public int viewType;
85
86        /** App-only properties */
87        // The section name of this app.  Note that there can be multiple items with different
88        // sectionNames in the same section
89        public String sectionName = null;
90        // The row that this item shows up on
91        public int rowIndex;
92        // The index of this app in the row
93        public int rowAppIndex;
94        // The associated AppInfo for the app
95        public AppInfo appInfo = null;
96        // The index of this app not including sections
97        public int appIndex = -1;
98
99        public static AdapterItem asPredictedApp(int pos, String sectionName, AppInfo appInfo,
100                int appIndex) {
101            AdapterItem item = asApp(pos, sectionName, appInfo, appIndex);
102            item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON;
103            return item;
104        }
105
106        public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
107                int appIndex) {
108            AdapterItem item = new AdapterItem();
109            item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON;
110            item.position = pos;
111            item.sectionName = sectionName;
112            item.appInfo = appInfo;
113            item.appIndex = appIndex;
114            return item;
115        }
116
117        public static AdapterItem asDiscoveryItem(int pos, String sectionName, AppInfo appInfo,
118                int appIndex) {
119            AdapterItem item = new AdapterItem();
120            item.viewType = AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM;
121            item.position = pos;
122            item.sectionName = sectionName;
123            item.appInfo = appInfo;
124            item.appIndex = appIndex;
125            return item;
126        }
127
128        public static AdapterItem asEmptySearch(int pos) {
129            AdapterItem item = new AdapterItem();
130            item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH;
131            item.position = pos;
132            return item;
133        }
134
135        public static AdapterItem asPredictionDivider(int pos) {
136            AdapterItem item = new AdapterItem();
137            item.viewType = AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER;
138            item.position = pos;
139            return item;
140        }
141
142        public static AdapterItem asMarketDivider(int pos) {
143            AdapterItem item = new AdapterItem();
144            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER;
145            item.position = pos;
146            return item;
147        }
148
149        public static AdapterItem asLoadingDivider(int pos) {
150            AdapterItem item = new AdapterItem();
151            item.viewType = AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER;
152            item.position = pos;
153            return item;
154        }
155
156        public static AdapterItem asMarketSearch(int pos) {
157            AdapterItem item = new AdapterItem();
158            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET;
159            item.position = pos;
160            return item;
161        }
162    }
163
164    private final Launcher mLauncher;
165
166    // The set of apps from the system not including predictions
167    private final List<AppInfo> mApps = new ArrayList<>();
168    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
169
170    // The set of filtered apps with the current filter
171    private final List<AppInfo> mFilteredApps = new ArrayList<>();
172    // The current set of adapter items
173    private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
174    // The set of sections that we allow fast-scrolling to (includes non-merged sections)
175    private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
176    // The set of predicted app component names
177    private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
178    // The set of predicted apps resolved from the component names and the current set of apps
179    private final List<AppInfo> mPredictedApps = new ArrayList<>();
180    private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>();
181
182    // The of ordered component names as a result of a search query
183    private ArrayList<ComponentKey> mSearchResults;
184    private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
185    private AllAppsGridAdapter mAdapter;
186    private AlphabeticIndexCompat mIndexer;
187    private AppInfoComparator mAppNameComparator;
188    private int mNumAppsPerRow;
189    private int mNumPredictedAppsPerRow;
190    private int mNumAppRowsInAdapter;
191
192    public AlphabeticalAppsList(Context context) {
193        mLauncher = Launcher.getLauncher(context);
194        mIndexer = new AlphabeticIndexCompat(context);
195        mAppNameComparator = new AppInfoComparator(context);
196    }
197
198    /**
199     * Sets the number of apps per row.
200     */
201    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
202        mNumAppsPerRow = numAppsPerRow;
203        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
204
205        updateAdapterItems();
206    }
207
208    /**
209     * Sets the adapter to notify when this dataset changes.
210     */
211    public void setAdapter(AllAppsGridAdapter adapter) {
212        mAdapter = adapter;
213    }
214
215    /**
216     * Returns all the apps.
217     */
218    public List<AppInfo> getApps() {
219        return mApps;
220    }
221
222    /**
223     * Returns the predicted apps.
224     */
225    public List<AppInfo> getPredictedApps() {
226        return mPredictedApps;
227    }
228
229    /**
230     * Returns fast scroller sections of all the current filtered applications.
231     */
232    public List<FastScrollSectionInfo> getFastScrollerSections() {
233        return mFastScrollerSections;
234    }
235
236    /**
237     * Returns the current filtered list of applications broken down into their sections.
238     */
239    public List<AdapterItem> getAdapterItems() {
240        return mAdapterItems;
241    }
242
243    /**
244     * Returns the number of rows of applications (not including predictions)
245     */
246    public int getNumAppRows() {
247        return mNumAppRowsInAdapter;
248    }
249
250    /**
251     * Returns the number of applications in this list.
252     */
253    public int getNumFilteredApps() {
254        return mFilteredApps.size();
255    }
256
257    /**
258     * Returns whether there are is a filter set.
259     */
260    public boolean hasFilter() {
261        return (mSearchResults != null);
262    }
263
264    /**
265     * Returns whether there are no filtered results.
266     */
267    public boolean hasNoFilteredResults() {
268        return (mSearchResults != null) && mFilteredApps.isEmpty();
269    }
270
271    boolean shouldShowEmptySearch() {
272        return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty();
273    }
274
275    /**
276     * Sets the sorted list of filtered components.
277     */
278    public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
279        if (mSearchResults != f) {
280            boolean same = mSearchResults != null && mSearchResults.equals(f);
281            mSearchResults = f;
282            updateAdapterItems();
283            return !same;
284        }
285        return false;
286    }
287
288    public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
289                @NonNull AppDiscoveryUpdateState state) {
290        mAppDiscoveryUpdateState = state;
291        switch (state) {
292            case START:
293                mDiscoveredApps.clear();
294                break;
295            case UPDATE:
296                mDiscoveredApps.add(new AppDiscoveryAppInfo(app));
297                break;
298        }
299        updateAdapterItems();
300    }
301
302    private List<AppInfo> processPredictedAppComponents(List<ComponentKeyMapper<AppInfo>> components) {
303        if (mComponentToAppMap.isEmpty()) {
304            // Apps have not been bound yet.
305            return Collections.emptyList();
306        }
307
308        List<AppInfo> predictedApps = new ArrayList<>();
309        for (ComponentKeyMapper<AppInfo> mapper : components) {
310            AppInfo info = mapper.getItem(mComponentToAppMap);
311            if (info != null) {
312                predictedApps.add(info);
313            } else {
314                if (FeatureFlags.IS_DOGFOOD_BUILD) {
315                    Log.e(TAG, "Predicted app not found: " + mapper);
316                }
317            }
318            // Stop at the number of predicted apps
319            if (predictedApps.size() == mNumPredictedAppsPerRow) {
320                break;
321            }
322        }
323        return predictedApps;
324    }
325
326    /**
327     * Sets the current set of predicted apps.
328     *
329     * This can be called before we get the full set of applications, we should merge the results
330     * only in onAppsUpdated() which is idempotent.
331     *
332     * If the number of predicted apps is the same as the previous list of predicted apps,
333     * we can optimize by swapping them in place.
334     */
335    public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
336        mPredictedAppComponents.clear();
337        mPredictedAppComponents.addAll(apps);
338
339        List<AppInfo> newPredictedApps = processPredictedAppComponents(apps);
340        // We only need to do work if any of the visible predicted apps have changed.
341        if (!newPredictedApps.equals(mPredictedApps)) {
342            if (newPredictedApps.size() == mPredictedApps.size()) {
343                swapInNewPredictedApps(newPredictedApps);
344            } else {
345                // We need to update the appIndex of all the items.
346                onAppsUpdated();
347            }
348        }
349    }
350
351    /**
352     * Swaps out the old predicted apps with the new predicted apps, in place. This optimization
353     * allows us to skip an entire relayout that would otherwise be called by notifyDataSetChanged.
354     *
355     * Note: This should only be called if the # of predicted apps is the same.
356     *       This method assumes that predicted apps are the first items in the adapter.
357     */
358    private void swapInNewPredictedApps(List<AppInfo> apps) {
359        mPredictedApps.clear();
360        mPredictedApps.addAll(apps);
361
362        int size = apps.size();
363        for (int i = 0; i < size; ++i) {
364            AppInfo info = apps.get(i);
365            AdapterItem appItem = AdapterItem.asPredictedApp(i, "", info, i);
366            appItem.rowAppIndex = i;
367            mAdapterItems.set(i, appItem);
368            mFilteredApps.set(i, info);
369            mAdapter.notifyItemChanged(i);
370        }
371    }
372
373    /**
374     * Sets the current set of apps.
375     */
376    public void setApps(List<AppInfo> apps) {
377        mComponentToAppMap.clear();
378        addOrUpdateApps(apps);
379    }
380
381    /**
382     * Adds or updates existing apps in the list
383     */
384    public void addOrUpdateApps(List<AppInfo> apps) {
385        for (AppInfo app : apps) {
386            mComponentToAppMap.put(app.toComponentKey(), app);
387        }
388        onAppsUpdated();
389    }
390
391    /**
392     * Removes some apps from the list.
393     */
394    public void removeApps(List<AppInfo> apps) {
395        for (AppInfo app : apps) {
396            mComponentToAppMap.remove(app.toComponentKey());
397        }
398        onAppsUpdated();
399    }
400
401    /**
402     * Updates internals when the set of apps are updated.
403     */
404    private void onAppsUpdated() {
405        // Sort the list of apps
406        mApps.clear();
407        mApps.addAll(mComponentToAppMap.values());
408        Collections.sort(mApps, mAppNameComparator);
409
410        // As a special case for some languages (currently only Simplified Chinese), we may need to
411        // coalesce sections
412        Locale curLocale = mLauncher.getResources().getConfiguration().locale;
413        boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
414        if (localeRequiresSectionSorting) {
415            // Compute the section headers. We use a TreeMap with the section name comparator to
416            // ensure that the sections are ordered when we iterate over it later
417            TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator());
418            for (AppInfo info : mApps) {
419                // Add the section to the cache
420                String sectionName = getAndUpdateCachedSectionName(info.title);
421
422                // Add it to the mapping
423                ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
424                if (sectionApps == null) {
425                    sectionApps = new ArrayList<>();
426                    sectionMap.put(sectionName, sectionApps);
427                }
428                sectionApps.add(info);
429            }
430
431            // Add each of the section apps to the list in order
432            mApps.clear();
433            for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
434                mApps.addAll(entry.getValue());
435            }
436        } else {
437            // Just compute the section headers for use below
438            for (AppInfo info : mApps) {
439                // Add the section to the cache
440                getAndUpdateCachedSectionName(info.title);
441            }
442        }
443
444        // Recompose the set of adapter items from the current set of apps
445        updateAdapterItems();
446    }
447
448    /**
449     * Updates the set of filtered apps with the current filter.  At this point, we expect
450     * mCachedSectionNames to have been calculated for the set of all apps in mApps.
451     */
452    private void updateAdapterItems() {
453        refillAdapterItems();
454        refreshRecyclerView();
455    }
456
457    private void refreshRecyclerView() {
458        if (mAdapter != null) {
459            mAdapter.notifyDataSetChanged();
460        }
461    }
462
463    private void refillAdapterItems() {
464        String lastSectionName = null;
465        FastScrollSectionInfo lastFastScrollerSectionInfo = null;
466        int position = 0;
467        int appIndex = 0;
468
469        // Prepare to update the list of sections, filtered apps, etc.
470        mFilteredApps.clear();
471        mFastScrollerSections.clear();
472        mAdapterItems.clear();
473
474        if (DEBUG_PREDICTIONS) {
475            if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
476                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
477                        Process.myUserHandle())));
478                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
479                        Process.myUserHandle())));
480                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
481                        Process.myUserHandle())));
482                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
483                        Process.myUserHandle())));
484            }
485        }
486
487        // Process the predicted app components
488        mPredictedApps.clear();
489        if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
490            mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
491
492            if (!mPredictedApps.isEmpty()) {
493                // Add a section for the predictions
494                lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
495                mFastScrollerSections.add(lastFastScrollerSectionInfo);
496
497                // Add the predicted app items
498                for (AppInfo info : mPredictedApps) {
499                    AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info,
500                            appIndex++);
501                    if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
502                        lastFastScrollerSectionInfo.fastScrollToItem = appItem;
503                    }
504                    mAdapterItems.add(appItem);
505                    mFilteredApps.add(info);
506                }
507
508                mAdapterItems.add(AdapterItem.asPredictionDivider(position++));
509            }
510        }
511
512        // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
513        // ordered set of sections
514        for (AppInfo info : getFiltersAppInfos()) {
515            String sectionName = getAndUpdateCachedSectionName(info.title);
516
517            // Create a new section if the section names do not match
518            if (!sectionName.equals(lastSectionName)) {
519                lastSectionName = sectionName;
520                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
521                mFastScrollerSections.add(lastFastScrollerSectionInfo);
522            }
523
524            // Create an app item
525            AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
526            if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
527                lastFastScrollerSectionInfo.fastScrollToItem = appItem;
528            }
529            mAdapterItems.add(appItem);
530            mFilteredApps.add(info);
531        }
532
533        if (hasFilter()) {
534            if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) {
535                mAdapterItems.add(AdapterItem.asLoadingDivider(position++));
536                // Append all app discovery results
537                for (int i = 0; i < mDiscoveredApps.size(); i++) {
538                    AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i);
539                    if (appDiscoveryAppInfo.isRecent) {
540                        // already handled in getFilteredAppInfos()
541                        continue;
542                    }
543                    AdapterItem item = AdapterItem.asDiscoveryItem(position++,
544                            "", appDiscoveryAppInfo, appIndex++);
545                    mAdapterItems.add(item);
546                }
547
548                if (!isAppDiscoveryRunning()) {
549                    mAdapterItems.add(AdapterItem.asMarketSearch(position++));
550                }
551            } else {
552                // Append the search market item
553                if (hasNoFilteredResults()) {
554                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
555                } else {
556                    mAdapterItems.add(AdapterItem.asMarketDivider(position++));
557                }
558                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
559            }
560        }
561
562        if (mNumAppsPerRow != 0) {
563            // Update the number of rows in the adapter after we do all the merging (otherwise, we
564            // would have to shift the values again)
565            int numAppsInSection = 0;
566            int numAppsInRow = 0;
567            int rowIndex = -1;
568            for (AdapterItem item : mAdapterItems) {
569                item.rowIndex = 0;
570                if (AllAppsGridAdapter.isDividerViewType(item.viewType)) {
571                    numAppsInSection = 0;
572                } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
573                    if (numAppsInSection % mNumAppsPerRow == 0) {
574                        numAppsInRow = 0;
575                        rowIndex++;
576                    }
577                    item.rowIndex = rowIndex;
578                    item.rowAppIndex = numAppsInRow;
579                    numAppsInSection++;
580                    numAppsInRow++;
581                }
582            }
583            mNumAppRowsInAdapter = rowIndex + 1;
584
585            // Pre-calculate all the fast scroller fractions
586            switch (mFastScrollDistributionMode) {
587                case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION:
588                    float rowFraction = 1f / mNumAppRowsInAdapter;
589                    for (FastScrollSectionInfo info : mFastScrollerSections) {
590                        AdapterItem item = info.fastScrollToItem;
591                        if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
592                            info.touchFraction = 0f;
593                            continue;
594                        }
595
596                        float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
597                        info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
598                    }
599                    break;
600                case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS:
601                    float perSectionTouchFraction = 1f / mFastScrollerSections.size();
602                    float cumulativeTouchFraction = 0f;
603                    for (FastScrollSectionInfo info : mFastScrollerSections) {
604                        AdapterItem item = info.fastScrollToItem;
605                        if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
606                            info.touchFraction = 0f;
607                            continue;
608                        }
609                        info.touchFraction = cumulativeTouchFraction;
610                        cumulativeTouchFraction += perSectionTouchFraction;
611                    }
612                    break;
613            }
614        }
615    }
616
617    public boolean isAppDiscoveryRunning() {
618        return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START
619                || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE;
620    }
621
622    private List<AppInfo> getFiltersAppInfos() {
623        if (mSearchResults == null) {
624            return mApps;
625        }
626
627        ArrayList<AppInfo> result = new ArrayList<>();
628        for (ComponentKey key : mSearchResults) {
629            AppInfo match = mComponentToAppMap.get(key);
630            if (match != null) {
631                result.add(match);
632            }
633        }
634
635        // adding recently used instant apps
636        if (mDiscoveredApps.size() > 0) {
637            for (int i = 0; i < mDiscoveredApps.size(); i++) {
638                AppDiscoveryAppInfo discoveryAppInfo = mDiscoveredApps.get(i);
639                if (discoveryAppInfo.isRecent) {
640                    result.add(discoveryAppInfo);
641                }
642            }
643            Collections.sort(result, mAppNameComparator);
644        }
645        return result;
646    }
647
648    public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
649        return mapper.getItem(mComponentToAppMap);
650    }
651
652    /**
653     * Returns the cached section name for the given title, recomputing and updating the cache if
654     * the title has no cached section name.
655     */
656    private String getAndUpdateCachedSectionName(CharSequence title) {
657        String sectionName = mCachedSectionNames.get(title);
658        if (sectionName == null) {
659            sectionName = mIndexer.computeSectionName(title);
660            mCachedSectionNames.put(title, sectionName);
661        }
662        return sectionName;
663    }
664
665}
666