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