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.content.pm.PackageManager;
20 
21 import com.android.launcher3.AppInfo;
22 import com.android.launcher3.Launcher;
23 import com.android.launcher3.Utilities;
24 import com.android.launcher3.shortcuts.DeepShortcutManager;
25 import com.android.launcher3.util.ComponentKey;
26 import com.android.launcher3.util.ItemInfoMatcher;
27 import com.android.launcher3.util.LabelComparator;
28 
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.TreeMap;
35 
36 /**
37  * The alphabetically sorted list of applications.
38  */
39 public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener {
40 
41     public static final String TAG = "AlphabeticalAppsList";
42 
43     private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0;
44     private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1;
45 
46     private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS;
47 
48     /**
49      * Info about a fast scroller section, depending if sections are merged, the fast scroller
50      * sections will not be the same set as the section headers.
51      */
52     public static class FastScrollSectionInfo {
53         // The section name
54         public String sectionName;
55         // The AdapterItem to scroll to for this section
56         public AdapterItem fastScrollToItem;
57         // The touch fraction that should map to this fast scroll section info
58         public float touchFraction;
59 
FastScrollSectionInfo(String sectionName)60         public FastScrollSectionInfo(String sectionName) {
61             this.sectionName = sectionName;
62         }
63     }
64 
65     /**
66      * Info about a particular adapter item (can be either section or app)
67      */
68     public static class AdapterItem {
69         /** Common properties */
70         // The index of this adapter item in the list
71         public int position;
72         // The type of this item
73         public int viewType;
74 
75         /** App-only properties */
76         // The section name of this app.  Note that there can be multiple items with different
77         // sectionNames in the same section
78         public String sectionName = null;
79         // The row that this item shows up on
80         public int rowIndex;
81         // The index of this app in the row
82         public int rowAppIndex;
83         // The associated AppInfo for the app
84         public AppInfo appInfo = null;
85         // The index of this app not including sections
86         public int appIndex = -1;
87 
asApp(int pos, String sectionName, AppInfo appInfo, int appIndex)88         public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
89                 int appIndex) {
90             AdapterItem item = new AdapterItem();
91             item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON;
92             item.position = pos;
93             item.sectionName = sectionName;
94             item.appInfo = appInfo;
95             item.appIndex = appIndex;
96             return item;
97         }
98 
asEmptySearch(int pos)99         public static AdapterItem asEmptySearch(int pos) {
100             AdapterItem item = new AdapterItem();
101             item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH;
102             item.position = pos;
103             return item;
104         }
105 
asAllAppsDivider(int pos)106         public static AdapterItem asAllAppsDivider(int pos) {
107             AdapterItem item = new AdapterItem();
108             item.viewType = AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER;
109             item.position = pos;
110             return item;
111         }
112 
asMarketSearch(int pos)113         public static AdapterItem asMarketSearch(int pos) {
114             AdapterItem item = new AdapterItem();
115             item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET;
116             item.position = pos;
117             return item;
118         }
119 
asWorkTabFooter(int pos)120         public static AdapterItem asWorkTabFooter(int pos) {
121             AdapterItem item = new AdapterItem();
122             item.viewType = AllAppsGridAdapter.VIEW_TYPE_WORK_TAB_FOOTER;
123             item.position = pos;
124             return item;
125         }
126     }
127 
128     private final Launcher mLauncher;
129 
130     // The set of apps from the system
131     private final List<AppInfo> mApps = new ArrayList<>();
132     private final AllAppsStore mAllAppsStore;
133 
134     // The set of filtered apps with the current filter
135     private final List<AppInfo> mFilteredApps = new ArrayList<>();
136     // The current set of adapter items
137     private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
138     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
139     private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
140     // Is it the work profile app list.
141     private final boolean mIsWork;
142 
143     // The of ordered component names as a result of a search query
144     private ArrayList<ComponentKey> mSearchResults;
145     private AllAppsGridAdapter mAdapter;
146     private AppInfoComparator mAppNameComparator;
147     private final int mNumAppsPerRow;
148     private int mNumAppRowsInAdapter;
149     private ItemInfoMatcher mItemFilter;
150 
AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork)151     public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) {
152         mAllAppsStore = appsStore;
153         mLauncher = Launcher.getLauncher(context);
154         mAppNameComparator = new AppInfoComparator(context);
155         mIsWork = isWork;
156         mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns;
157         mAllAppsStore.addUpdateListener(this);
158     }
159 
updateItemFilter(ItemInfoMatcher itemFilter)160     public void updateItemFilter(ItemInfoMatcher itemFilter) {
161         this.mItemFilter = itemFilter;
162         onAppsUpdated();
163     }
164 
165     /**
166      * Sets the adapter to notify when this dataset changes.
167      */
setAdapter(AllAppsGridAdapter adapter)168     public void setAdapter(AllAppsGridAdapter adapter) {
169         mAdapter = adapter;
170     }
171 
172     /**
173      * Returns all the apps.
174      */
getApps()175     public List<AppInfo> getApps() {
176         return mApps;
177     }
178 
179     /**
180      * Returns fast scroller sections of all the current filtered applications.
181      */
getFastScrollerSections()182     public List<FastScrollSectionInfo> getFastScrollerSections() {
183         return mFastScrollerSections;
184     }
185 
186     /**
187      * Returns the current filtered list of applications broken down into their sections.
188      */
getAdapterItems()189     public List<AdapterItem> getAdapterItems() {
190         return mAdapterItems;
191     }
192 
193     /**
194      * Returns the number of rows of applications
195      */
getNumAppRows()196     public int getNumAppRows() {
197         return mNumAppRowsInAdapter;
198     }
199 
200     /**
201      * Returns the number of applications in this list.
202      */
getNumFilteredApps()203     public int getNumFilteredApps() {
204         return mFilteredApps.size();
205     }
206 
207     /**
208      * Returns whether there are is a filter set.
209      */
hasFilter()210     public boolean hasFilter() {
211         return (mSearchResults != null);
212     }
213 
214     /**
215      * Returns whether there are no filtered results.
216      */
hasNoFilteredResults()217     public boolean hasNoFilteredResults() {
218         return (mSearchResults != null) && mFilteredApps.isEmpty();
219     }
220 
221     /**
222      * Sets the sorted list of filtered components.
223      */
setOrderedFilter(ArrayList<ComponentKey> f)224     public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
225         if (mSearchResults != f) {
226             boolean same = mSearchResults != null && mSearchResults.equals(f);
227             mSearchResults = f;
228             onAppsUpdated();
229             return !same;
230         }
231         return false;
232     }
233 
234     /**
235      * Updates internals when the set of apps are updated.
236      */
237     @Override
onAppsUpdated()238     public void onAppsUpdated() {
239         // Sort the list of apps
240         mApps.clear();
241 
242         for (AppInfo app : mAllAppsStore.getApps()) {
243             if (mItemFilter == null || mItemFilter.matches(app, null) || hasFilter()) {
244                 mApps.add(app);
245             }
246         }
247 
248         Collections.sort(mApps, mAppNameComparator);
249 
250         // As a special case for some languages (currently only Simplified Chinese), we may need to
251         // coalesce sections
252         Locale curLocale = mLauncher.getResources().getConfiguration().locale;
253         boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
254         if (localeRequiresSectionSorting) {
255             // Compute the section headers. We use a TreeMap with the section name comparator to
256             // ensure that the sections are ordered when we iterate over it later
257             TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator());
258             for (AppInfo info : mApps) {
259                 // Add the section to the cache
260                 String sectionName = info.sectionName;
261 
262                 // Add it to the mapping
263                 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
264                 if (sectionApps == null) {
265                     sectionApps = new ArrayList<>();
266                     sectionMap.put(sectionName, sectionApps);
267                 }
268                 sectionApps.add(info);
269             }
270 
271             // Add each of the section apps to the list in order
272             mApps.clear();
273             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
274                 mApps.addAll(entry.getValue());
275             }
276         }
277 
278         // Recompose the set of adapter items from the current set of apps
279         updateAdapterItems();
280     }
281 
282     /**
283      * Updates the set of filtered apps with the current filter.  At this point, we expect
284      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
285      */
updateAdapterItems()286     private void updateAdapterItems() {
287         refillAdapterItems();
288         refreshRecyclerView();
289     }
290 
refreshRecyclerView()291     private void refreshRecyclerView() {
292         if (mAdapter != null) {
293             mAdapter.notifyDataSetChanged();
294         }
295     }
296 
refillAdapterItems()297     private void refillAdapterItems() {
298         String lastSectionName = null;
299         FastScrollSectionInfo lastFastScrollerSectionInfo = null;
300         int position = 0;
301         int appIndex = 0;
302 
303         // Prepare to update the list of sections, filtered apps, etc.
304         mFilteredApps.clear();
305         mFastScrollerSections.clear();
306         mAdapterItems.clear();
307 
308         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
309         // ordered set of sections
310         for (AppInfo info : getFiltersAppInfos()) {
311             String sectionName = info.sectionName;
312 
313             // Create a new section if the section names do not match
314             if (!sectionName.equals(lastSectionName)) {
315                 lastSectionName = sectionName;
316                 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
317                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
318             }
319 
320             // Create an app item
321             AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
322             if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
323                 lastFastScrollerSectionInfo.fastScrollToItem = appItem;
324             }
325             mAdapterItems.add(appItem);
326             mFilteredApps.add(info);
327         }
328 
329         if (hasFilter()) {
330             // Append the search market item
331             if (hasNoFilteredResults()) {
332                 mAdapterItems.add(AdapterItem.asEmptySearch(position++));
333             } else {
334                 mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
335             }
336             mAdapterItems.add(AdapterItem.asMarketSearch(position++));
337         }
338 
339         if (mNumAppsPerRow != 0) {
340             // Update the number of rows in the adapter after we do all the merging (otherwise, we
341             // would have to shift the values again)
342             int numAppsInSection = 0;
343             int numAppsInRow = 0;
344             int rowIndex = -1;
345             for (AdapterItem item : mAdapterItems) {
346                 item.rowIndex = 0;
347                 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) {
348                     numAppsInSection = 0;
349                 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
350                     if (numAppsInSection % mNumAppsPerRow == 0) {
351                         numAppsInRow = 0;
352                         rowIndex++;
353                     }
354                     item.rowIndex = rowIndex;
355                     item.rowAppIndex = numAppsInRow;
356                     numAppsInSection++;
357                     numAppsInRow++;
358                 }
359             }
360             mNumAppRowsInAdapter = rowIndex + 1;
361 
362             // Pre-calculate all the fast scroller fractions
363             switch (mFastScrollDistributionMode) {
364                 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION:
365                     float rowFraction = 1f / mNumAppRowsInAdapter;
366                     for (FastScrollSectionInfo info : mFastScrollerSections) {
367                         AdapterItem item = info.fastScrollToItem;
368                         if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
369                             info.touchFraction = 0f;
370                             continue;
371                         }
372 
373                         float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
374                         info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
375                     }
376                     break;
377                 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS:
378                     float perSectionTouchFraction = 1f / mFastScrollerSections.size();
379                     float cumulativeTouchFraction = 0f;
380                     for (FastScrollSectionInfo info : mFastScrollerSections) {
381                         AdapterItem item = info.fastScrollToItem;
382                         if (!AllAppsGridAdapter.isIconViewType(item.viewType)) {
383                             info.touchFraction = 0f;
384                             continue;
385                         }
386                         info.touchFraction = cumulativeTouchFraction;
387                         cumulativeTouchFraction += perSectionTouchFraction;
388                     }
389                     break;
390             }
391         }
392 
393         // Add the work profile footer if required.
394         if (shouldShowWorkFooter()) {
395             mAdapterItems.add(AdapterItem.asWorkTabFooter(position++));
396         }
397     }
398 
shouldShowWorkFooter()399     private boolean shouldShowWorkFooter() {
400         return mIsWork && Utilities.ATLEAST_P &&
401                 (DeepShortcutManager.getInstance(mLauncher).hasHostPermission()
402                         || mLauncher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
403                         == PackageManager.PERMISSION_GRANTED);
404     }
405 
getFiltersAppInfos()406     private List<AppInfo> getFiltersAppInfos() {
407         if (mSearchResults == null) {
408             return mApps;
409         }
410         ArrayList<AppInfo> result = new ArrayList<>();
411         for (ComponentKey key : mSearchResults) {
412             AppInfo match = mAllAppsStore.getApp(key);
413             if (match != null) {
414                 result.add(match);
415             }
416         }
417         return result;
418     }
419 }
420