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  */
16 package com.android.launcher3.allapps;
17 
18 import android.content.Context;
19 import android.os.Process;
20 import android.support.annotation.NonNull;
21 import android.support.annotation.Nullable;
22 import android.util.Log;
23 
24 import com.android.launcher3.AppInfo;
25 import com.android.launcher3.Launcher;
26 import com.android.launcher3.compat.AlphabeticIndexCompat;
27 import com.android.launcher3.config.ProviderConfig;
28 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
29 import com.android.launcher3.discovery.AppDiscoveryItem;
30 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
31 import com.android.launcher3.util.ComponentKey;
32 import com.android.launcher3.util.LabelComparator;
33 
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.TreeMap;
41 
42 /**
43  * The alphabetically sorted list of applications.
44  */
45 public 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 
FastScrollSectionInfo(String sectionName)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 
asPredictedApp(int pos, String sectionName, AppInfo appInfo, int appIndex)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 
asApp(int pos, String sectionName, AppInfo appInfo, int appIndex)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 
asDiscoveryItem(int pos, String sectionName, AppInfo appInfo, int appIndex)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 
asEmptySearch(int pos)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 
asPredictionDivider(int pos)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 
asSearchDivider(int pos)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 
asMarketDivider(int pos)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 
asLoadingDivider(int pos)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 
asMarketSearch(int pos)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     private final List<AppDiscoveryAppInfo> mDiscoveredApps = new ArrayList<>();
187 
188     // The of ordered component names as a result of a search query
189     private ArrayList<ComponentKey> mSearchResults;
190     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
191     private AllAppsGridAdapter mAdapter;
192     private AlphabeticIndexCompat mIndexer;
193     private AppInfoComparator mAppNameComparator;
194     private int mNumAppsPerRow;
195     private int mNumPredictedAppsPerRow;
196     private int mNumAppRowsInAdapter;
197 
AlphabeticalAppsList(Context context)198     public AlphabeticalAppsList(Context context) {
199         mLauncher = Launcher.getLauncher(context);
200         mIndexer = new AlphabeticIndexCompat(context);
201         mAppNameComparator = new AppInfoComparator(context);
202     }
203 
204     /**
205      * Sets the number of apps per row.
206      */
setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow)207     public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
208         mNumAppsPerRow = numAppsPerRow;
209         mNumPredictedAppsPerRow = numPredictedAppsPerRow;
210 
211         updateAdapterItems();
212     }
213 
214     /**
215      * Sets the adapter to notify when this dataset changes.
216      */
setAdapter(AllAppsGridAdapter adapter)217     public void setAdapter(AllAppsGridAdapter adapter) {
218         mAdapter = adapter;
219     }
220 
221     /**
222      * Returns all the apps.
223      */
getApps()224     public List<AppInfo> getApps() {
225         return mApps;
226     }
227 
228     /**
229      * Returns fast scroller sections of all the current filtered applications.
230      */
getFastScrollerSections()231     public List<FastScrollSectionInfo> getFastScrollerSections() {
232         return mFastScrollerSections;
233     }
234 
235     /**
236      * Returns the current filtered list of applications broken down into their sections.
237      */
getAdapterItems()238     public List<AdapterItem> getAdapterItems() {
239         return mAdapterItems;
240     }
241 
242     /**
243      * Returns the number of rows of applications (not including predictions)
244      */
getNumAppRows()245     public int getNumAppRows() {
246         return mNumAppRowsInAdapter;
247     }
248 
249     /**
250      * Returns the number of applications in this list.
251      */
getNumFilteredApps()252     public int getNumFilteredApps() {
253         return mFilteredApps.size();
254     }
255 
256     /**
257      * Returns whether there are is a filter set.
258      */
hasFilter()259     public boolean hasFilter() {
260         return (mSearchResults != null);
261     }
262 
263     /**
264      * Returns whether there are no filtered results.
265      */
hasNoFilteredResults()266     public boolean hasNoFilteredResults() {
267         return (mSearchResults != null) && mFilteredApps.isEmpty();
268     }
269 
shouldShowEmptySearch()270     boolean shouldShowEmptySearch() {
271         return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty();
272     }
273 
274     /**
275      * Sets the sorted list of filtered components.
276      */
setOrderedFilter(ArrayList<ComponentKey> f)277     public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
278         if (mSearchResults != f) {
279             boolean same = mSearchResults != null && mSearchResults.equals(f);
280             mSearchResults = f;
281             updateAdapterItems();
282             return !same;
283         }
284         return false;
285     }
286 
onAppDiscoverySearchUpdate(@ullable AppDiscoveryItem app, @NonNull AppDiscoveryUpdateState state)287     public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
288                 @NonNull AppDiscoveryUpdateState state) {
289         mAppDiscoveryUpdateState = state;
290         switch (state) {
291             case START:
292                 mDiscoveredApps.clear();
293                 break;
294             case UPDATE:
295                 mDiscoveredApps.add(new AppDiscoveryAppInfo(app));
296                 break;
297         }
298         updateAdapterItems();
299     }
300 
301     /**
302      * Sets the current set of predicted apps.  Since this can be called before we get the full set
303      * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
304      */
setPredictedApps(List<ComponentKey> apps)305     public void setPredictedApps(List<ComponentKey> apps) {
306         mPredictedAppComponents.clear();
307         mPredictedAppComponents.addAll(apps);
308         onAppsUpdated();
309     }
310 
311     /**
312      * Sets the current set of apps.
313      */
setApps(List<AppInfo> apps)314     public void setApps(List<AppInfo> apps) {
315         mComponentToAppMap.clear();
316         addApps(apps);
317     }
318 
319     /**
320      * Adds new apps to the list.
321      */
addApps(List<AppInfo> apps)322     public void addApps(List<AppInfo> apps) {
323         updateApps(apps);
324     }
325 
326     /**
327      * Updates existing apps in the list
328      */
updateApps(List<AppInfo> apps)329     public void updateApps(List<AppInfo> apps) {
330         for (AppInfo app : apps) {
331             mComponentToAppMap.put(app.toComponentKey(), app);
332         }
333         onAppsUpdated();
334     }
335 
336     /**
337      * Removes some apps from the list.
338      */
removeApps(List<AppInfo> apps)339     public void removeApps(List<AppInfo> apps) {
340         for (AppInfo app : apps) {
341             mComponentToAppMap.remove(app.toComponentKey());
342         }
343         onAppsUpdated();
344     }
345 
346     /**
347      * Updates internals when the set of apps are updated.
348      */
onAppsUpdated()349     private void onAppsUpdated() {
350         // Sort the list of apps
351         mApps.clear();
352         mApps.addAll(mComponentToAppMap.values());
353         Collections.sort(mApps, mAppNameComparator);
354 
355         // As a special case for some languages (currently only Simplified Chinese), we may need to
356         // coalesce sections
357         Locale curLocale = mLauncher.getResources().getConfiguration().locale;
358         boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
359         if (localeRequiresSectionSorting) {
360             // Compute the section headers. We use a TreeMap with the section name comparator to
361             // ensure that the sections are ordered when we iterate over it later
362             TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator());
363             for (AppInfo info : mApps) {
364                 // Add the section to the cache
365                 String sectionName = getAndUpdateCachedSectionName(info.title);
366 
367                 // Add it to the mapping
368                 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
369                 if (sectionApps == null) {
370                     sectionApps = new ArrayList<>();
371                     sectionMap.put(sectionName, sectionApps);
372                 }
373                 sectionApps.add(info);
374             }
375 
376             // Add each of the section apps to the list in order
377             mApps.clear();
378             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
379                 mApps.addAll(entry.getValue());
380             }
381         } else {
382             // Just compute the section headers for use below
383             for (AppInfo info : mApps) {
384                 // Add the section to the cache
385                 getAndUpdateCachedSectionName(info.title);
386             }
387         }
388 
389         // Recompose the set of adapter items from the current set of apps
390         updateAdapterItems();
391     }
392 
393     /**
394      * Updates the set of filtered apps with the current filter.  At this point, we expect
395      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
396      */
updateAdapterItems()397     private void updateAdapterItems() {
398         refillAdapterItems();
399         refreshRecyclerView();
400     }
401 
refreshRecyclerView()402     private void refreshRecyclerView() {
403         if (mAdapter != null) {
404             mAdapter.notifyDataSetChanged();
405         }
406     }
407 
refillAdapterItems()408     private void refillAdapterItems() {
409         String lastSectionName = null;
410         FastScrollSectionInfo lastFastScrollerSectionInfo = null;
411         int position = 0;
412         int appIndex = 0;
413 
414         // Prepare to update the list of sections, filtered apps, etc.
415         mFilteredApps.clear();
416         mFastScrollerSections.clear();
417         mAdapterItems.clear();
418 
419         if (DEBUG_PREDICTIONS) {
420             if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
421                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
422                         Process.myUserHandle()));
423                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
424                         Process.myUserHandle()));
425                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
426                         Process.myUserHandle()));
427                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
428                         Process.myUserHandle()));
429             }
430         }
431 
432         // Add the search divider
433         mAdapterItems.add(AdapterItem.asSearchDivider(position++));
434 
435         // Process the predicted app components
436         mPredictedApps.clear();
437         if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
438             for (ComponentKey ck : mPredictedAppComponents) {
439                 AppInfo info = mComponentToAppMap.get(ck);
440                 if (info != null) {
441                     mPredictedApps.add(info);
442                 } else {
443                     if (ProviderConfig.IS_DOGFOOD_BUILD) {
444                         Log.e(TAG, "Predicted app not found: " + ck);
445                     }
446                 }
447                 // Stop at the number of predicted apps
448                 if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
449                     break;
450                 }
451             }
452 
453             if (!mPredictedApps.isEmpty()) {
454                 // Add a section for the predictions
455                 lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
456                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
457 
458                 // Add the predicted app items
459                 for (AppInfo info : mPredictedApps) {
460                     AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info,
461                             appIndex++);
462                     if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
463                         lastFastScrollerSectionInfo.fastScrollToItem = appItem;
464                     }
465                     mAdapterItems.add(appItem);
466                     mFilteredApps.add(info);
467                 }
468 
469                 mAdapterItems.add(AdapterItem.asPredictionDivider(position++));
470             }
471         }
472 
473         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
474         // ordered set of sections
475         for (AppInfo info : getFiltersAppInfos()) {
476             String sectionName = getAndUpdateCachedSectionName(info.title);
477 
478             // Create a new section if the section names do not match
479             if (!sectionName.equals(lastSectionName)) {
480                 lastSectionName = sectionName;
481                 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
482                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
483             }
484 
485             // Create an app item
486             AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
487             if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
488                 lastFastScrollerSectionInfo.fastScrollToItem = appItem;
489             }
490             mAdapterItems.add(appItem);
491             mFilteredApps.add(info);
492         }
493 
494         if (hasFilter()) {
495             if (isAppDiscoveryRunning() || mDiscoveredApps.size() > 0) {
496                 mAdapterItems.add(AdapterItem.asLoadingDivider(position++));
497                 // Append all app discovery results
498                 for (int i = 0; i < mDiscoveredApps.size(); i++) {
499                     AppDiscoveryAppInfo appDiscoveryAppInfo = mDiscoveredApps.get(i);
500                     if (appDiscoveryAppInfo.isRecent) {
501                         // already handled in getFilteredAppInfos()
502                         continue;
503                     }
504                     AdapterItem item = AdapterItem.asDiscoveryItem(position++,
505                             "", appDiscoveryAppInfo, appIndex++);
506                     mAdapterItems.add(item);
507                 }
508 
509                 if (!isAppDiscoveryRunning()) {
510                     mAdapterItems.add(AdapterItem.asMarketSearch(position++));
511                 }
512             } else {
513                 // Append the search market item
514                 if (hasNoFilteredResults()) {
515                     mAdapterItems.add(AdapterItem.asEmptySearch(position++));
516                 } else {
517                     mAdapterItems.add(AdapterItem.asMarketDivider(position++));
518                 }
519                 mAdapterItems.add(AdapterItem.asMarketSearch(position++));
520             }
521         }
522 
523         if (mNumAppsPerRow != 0) {
524             // Update the number of rows in the adapter after we do all the merging (otherwise, we
525             // would have to shift the values again)
526             int numAppsInSection = 0;
527             int numAppsInRow = 0;
528             int rowIndex = -1;
529             for (AdapterItem item : mAdapterItems) {
530                 item.rowIndex = 0;
531                 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) {
532                     numAppsInSection = 0;
533                 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
534                     if (numAppsInSection % mNumAppsPerRow == 0) {
535                         numAppsInRow = 0;
536                         rowIndex++;
537                     }
538                     item.rowIndex = rowIndex;
539                     item.rowAppIndex = numAppsInRow;
540                     numAppsInSection++;
541                     numAppsInRow++;
542                 }
543             }
544             mNumAppRowsInAdapter = rowIndex + 1;
545 
546             // Pre-calculate all the fast scroller fractions
547             switch (mFastScrollDistributionMode) {
548                 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION:
549                     float rowFraction = 1f / mNumAppRowsInAdapter;
550                     for (FastScrollSectionInfo info : mFastScrollerSections) {
551                         AdapterItem item = info.fastScrollToItem;
552                         if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
553                             info.touchFraction = 0f;
554                             continue;
555                         }
556 
557                         float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
558                         info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
559                     }
560                     break;
561                 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS:
562                     float perSectionTouchFraction = 1f / mFastScrollerSections.size();
563                     float cumulativeTouchFraction = 0f;
564                     for (FastScrollSectionInfo info : mFastScrollerSections) {
565                         AdapterItem item = info.fastScrollToItem;
566                         if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
567                             info.touchFraction = 0f;
568                             continue;
569                         }
570                         info.touchFraction = cumulativeTouchFraction;
571                         cumulativeTouchFraction += perSectionTouchFraction;
572                     }
573                     break;
574             }
575         }
576     }
577 
isAppDiscoveryRunning()578     public boolean isAppDiscoveryRunning() {
579         return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START
580                 || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE;
581     }
582 
getFiltersAppInfos()583     private List<AppInfo> getFiltersAppInfos() {
584         if (mSearchResults == null) {
585             return mApps;
586         }
587 
588         ArrayList<AppInfo> result = new ArrayList<>();
589         for (ComponentKey key : mSearchResults) {
590             AppInfo match = mComponentToAppMap.get(key);
591             if (match != null) {
592                 result.add(match);
593             }
594         }
595 
596         // adding recently used instant apps
597         if (mDiscoveredApps.size() > 0) {
598             for (int i = 0; i < mDiscoveredApps.size(); i++) {
599                 AppDiscoveryAppInfo discoveryAppInfo = mDiscoveredApps.get(i);
600                 if (discoveryAppInfo.isRecent) {
601                     result.add(discoveryAppInfo);
602                 }
603             }
604             Collections.sort(result, mAppNameComparator);
605         }
606         return result;
607     }
608 
609     /**
610      * Returns the cached section name for the given title, recomputing and updating the cache if
611      * the title has no cached section name.
612      */
getAndUpdateCachedSectionName(CharSequence title)613     private String getAndUpdateCachedSectionName(CharSequence title) {
614         String sectionName = mCachedSectionNames.get(title);
615         if (sectionName == null) {
616             sectionName = mIndexer.computeSectionName(title);
617             mCachedSectionNames.put(title, sectionName);
618         }
619         return sectionName;
620     }
621 
622 }
623