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 static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
19 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
20 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT;
23 
24 import android.content.Context;
25 import android.text.Spannable;
26 import android.text.SpannableString;
27 import android.text.style.ImageSpan;
28 
29 import androidx.annotation.Nullable;
30 import androidx.annotation.VisibleForTesting;
31 import androidx.recyclerview.widget.DiffUtil;
32 
33 import com.android.launcher3.Flags;
34 import com.android.launcher3.R;
35 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
36 import com.android.launcher3.model.data.AppInfo;
37 import com.android.launcher3.model.data.ItemInfo;
38 import com.android.launcher3.util.LabelComparator;
39 import com.android.launcher3.views.ActivityContext;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.TreeMap;
47 import java.util.function.Predicate;
48 import java.util.stream.Collectors;
49 import java.util.stream.Stream;
50 
51 /**
52  * The alphabetically sorted list of applications.
53  *
54  * @param <T> Type of context inflating this view.
55  */
56 public class AlphabeticalAppsList<T extends Context & ActivityContext> implements
57         AllAppsStore.OnUpdateListener {
58 
59     public static final String TAG = "AlphabeticalAppsList";
60 
61     private final WorkProfileManager mWorkProviderManager;
62 
63     private final PrivateProfileManager mPrivateProviderManager;
64 
65     /**
66      * Info about a fast scroller section, depending if sections are merged, the fast scroller
67      * sections will not be the same set as the section headers.
68      */
69     public static class FastScrollSectionInfo {
70         // The section name
71         public final CharSequence sectionName;
72         // The item position
73         public final int position;
74 
FastScrollSectionInfo(CharSequence sectionName, int position)75         public FastScrollSectionInfo(CharSequence sectionName, int position) {
76             this.sectionName = sectionName;
77             this.position = position;
78         }
79     }
80 
81 
82     private final T mActivityContext;
83 
84     // The set of apps from the system
85     private final List<AppInfo> mApps = new ArrayList<>();
86     private final List<AppInfo> mPrivateApps = new ArrayList<>();
87     @Nullable
88     private final AllAppsStore<T> mAllAppsStore;
89 
90     // The number of results in current adapter
91     private int mAccessibilityResultsCount = 0;
92     // The current set of adapter items
93     private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>();
94     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
95     private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
96 
97     // The of ordered component names as a result of a search query
98     private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>();
99     private final SpannableString mPrivateProfileAppScrollerBadge;
100     private BaseAllAppsAdapter<T> mAdapter;
101     private AppInfoComparator mAppNameComparator;
102     private int mNumAppsPerRowAllApps;
103     private int mNumAppRowsInAdapter;
104     private Predicate<ItemInfo> mItemFilter;
105 
AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore, WorkProfileManager workProfileManager, PrivateProfileManager privateProfileManager)106     public AlphabeticalAppsList(Context context, @Nullable AllAppsStore<T> appsStore,
107             WorkProfileManager workProfileManager, PrivateProfileManager privateProfileManager) {
108         mAllAppsStore = appsStore;
109         mActivityContext = ActivityContext.lookupContext(context);
110         mAppNameComparator = new AppInfoComparator(context);
111         mWorkProviderManager = workProfileManager;
112         mPrivateProviderManager = privateProfileManager;
113         mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
114         if (mAllAppsStore != null) {
115             mAllAppsStore.addUpdateListener(this);
116         }
117         mPrivateProfileAppScrollerBadge = new SpannableString(" ");
118         mPrivateProfileAppScrollerBadge.setSpan(new ImageSpan(context,
119                         R.drawable.ic_private_profile_app_scroller_badge, ImageSpan.ALIGN_CENTER),
120                 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
121     }
122 
123     /** Set the number of apps per row when device profile changes. */
setNumAppsPerRowAllApps(int numAppsPerRow)124     public void setNumAppsPerRowAllApps(int numAppsPerRow) {
125         mNumAppsPerRowAllApps = numAppsPerRow;
126     }
127 
updateItemFilter(Predicate<ItemInfo> itemFilter)128     public void updateItemFilter(Predicate<ItemInfo> itemFilter) {
129         this.mItemFilter = itemFilter;
130         onAppsUpdated();
131     }
132 
133     /**
134      * Sets the adapter to notify when this dataset changes.
135      */
setAdapter(BaseAllAppsAdapter<T> adapter)136     public void setAdapter(BaseAllAppsAdapter<T> adapter) {
137         mAdapter = adapter;
138     }
139 
140     /**
141      * Returns fast scroller sections of all the current filtered applications.
142      */
getFastScrollerSections()143     public List<FastScrollSectionInfo> getFastScrollerSections() {
144         return mFastScrollerSections;
145     }
146 
147     /**
148      * Returns the current filtered list of applications broken down into their sections.
149      */
getAdapterItems()150     public List<AdapterItem> getAdapterItems() {
151         return mAdapterItems;
152     }
153 
154     /**
155      * Returns the child adapter item with IME launch focus.
156      */
getFocusedChild()157     public AdapterItem getFocusedChild() {
158         if (mAdapterItems.size() == 0 || getFocusedChildIndex() == -1) {
159             return null;
160         }
161         return mAdapterItems.get(getFocusedChildIndex());
162     }
163 
164     /**
165      * Returns the index of the child with IME launch focus.
166      */
getFocusedChildIndex()167     public int getFocusedChildIndex() {
168         for (AdapterItem item : mAdapterItems) {
169             if (item.isCountedForAccessibility()) {
170                 return mAdapterItems.indexOf(item);
171             }
172         }
173         return -1;
174     }
175 
176     /**
177      * Returns the number of rows of applications
178      */
getNumAppRows()179     public int getNumAppRows() {
180         return mNumAppRowsInAdapter;
181     }
182 
183     /**
184      * Returns the number of applications in this list.
185      */
getNumFilteredApps()186     public int getNumFilteredApps() {
187         return mAccessibilityResultsCount;
188     }
189 
190     /**
191      * Returns whether there are search results which will hide the A-Z list.
192      */
hasSearchResults()193     public boolean hasSearchResults() {
194         return !mSearchResults.isEmpty();
195     }
196 
197     /**
198      * Sets results list for search
199      */
setSearchResults(ArrayList<AdapterItem> results)200     public boolean setSearchResults(ArrayList<AdapterItem> results) {
201         if (Objects.equals(results, mSearchResults)) {
202             return false;
203         }
204         mSearchResults.clear();
205         if (results != null) {
206             mSearchResults.addAll(results);
207         }
208         updateAdapterItems();
209         return true;
210     }
211 
212     /**
213      * Updates internals when the set of apps are updated.
214      */
215     @Override
onAppsUpdated()216     public void onAppsUpdated() {
217         // Don't update apps when the private profile animations are running, otherwise the motion
218         // is canceled.
219         if (mAllAppsStore == null || (mPrivateProviderManager != null &&
220                 mPrivateProviderManager.getAnimationRunning())) {
221             return;
222         }
223         // Sort the list of apps
224         mApps.clear();
225         mPrivateApps.clear();
226 
227         Stream<AppInfo> appSteam = Stream.of(mAllAppsStore.getApps());
228         Stream<AppInfo> privateAppStream = Stream.of(mAllAppsStore.getApps());
229 
230         if (!hasSearchResults() && mItemFilter != null) {
231             appSteam = appSteam.filter(mItemFilter);
232             if (mPrivateProviderManager != null) {
233                 privateAppStream = privateAppStream
234                         .filter(mPrivateProviderManager.getItemInfoMatcher());
235             }
236         }
237         appSteam = appSteam.sorted(mAppNameComparator);
238         privateAppStream = privateAppStream.sorted(mAppNameComparator);
239 
240         // As a special case for some languages (currently only Simplified Chinese), we may need to
241         // coalesce sections
242         Locale curLocale = mActivityContext.getResources().getConfiguration().locale;
243         boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
244         if (localeRequiresSectionSorting) {
245             // Compute the section headers. We use a TreeMap with the section name comparator to
246             // ensure that the sections are ordered when we iterate over it later
247             appSteam = appSteam.collect(Collectors.groupingBy(
248                     info -> info.sectionName,
249                     () -> new TreeMap<>(new LabelComparator()),
250                     Collectors.toCollection(ArrayList::new)))
251                     .values()
252                     .stream()
253                     .flatMap(ArrayList::stream);
254         }
255 
256         appSteam.forEachOrdered(mApps::add);
257         privateAppStream.forEachOrdered(mPrivateApps::add);
258         // Recompose the set of adapter items from the current set of apps
259         if (mSearchResults.isEmpty()) {
260             updateAdapterItems();
261         }
262     }
263 
264     /**
265      * Updates the set of filtered apps with the current filter. At this point, we expect
266      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
267      */
updateAdapterItems()268     public void updateAdapterItems() {
269         List<AdapterItem> oldItems = new ArrayList<>(mAdapterItems);
270         // Prepare to update the list of sections, filtered apps, etc.
271         mFastScrollerSections.clear();
272         mAdapterItems.clear();
273         mAccessibilityResultsCount = 0;
274 
275         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
276         // ordered set of sections
277         if (hasSearchResults()) {
278             mAdapterItems.addAll(mSearchResults);
279         } else {
280             int position = 0;
281             boolean addApps = true;
282             if (mWorkProviderManager != null) {
283                 position += mWorkProviderManager.addWorkItems(mAdapterItems);
284                 addApps = mWorkProviderManager.shouldShowWorkApps();
285             }
286             if (addApps) {
287                 if (/* education card was added */ position == 1) {
288                     // Add work educard section with "info icon" at 0th position.
289                     mFastScrollerSections.add(new FastScrollSectionInfo(
290                             mActivityContext.getResources().getString(
291                                     R.string.work_profile_edu_section), 0));
292                 }
293                 position = addAppsWithSections(mApps, position);
294             }
295             if (Flags.enablePrivateSpace()) {
296                 position = addPrivateSpaceItems(position);
297             }
298         }
299         mAccessibilityResultsCount = (int) mAdapterItems.stream()
300                 .filter(AdapterItem::isCountedForAccessibility).count();
301 
302         if (mNumAppsPerRowAllApps != 0) {
303             // Update the number of rows in the adapter after we do all the merging (otherwise, we
304             // would have to shift the values again)
305             int numAppsInSection = 0;
306             int numAppsInRow = 0;
307             int rowIndex = -1;
308             for (AdapterItem item : mAdapterItems) {
309                 item.rowIndex = 0;
310                 if (BaseAllAppsAdapter.isDividerViewType(item.viewType)
311                         || BaseAllAppsAdapter.isPrivateSpaceHeaderView(item.viewType)
312                         || BaseAllAppsAdapter.isPrivateSpaceSysAppsDividerView(item.viewType)) {
313                     numAppsInSection = 0;
314                 } else if (BaseAllAppsAdapter.isIconViewType(item.viewType)) {
315                     if (numAppsInSection % mNumAppsPerRowAllApps == 0) {
316                         numAppsInRow = 0;
317                         rowIndex++;
318                     }
319                     item.rowIndex = rowIndex;
320                     item.rowAppIndex = numAppsInRow;
321                     numAppsInSection++;
322                     numAppsInRow++;
323                 }
324             }
325             mNumAppRowsInAdapter = rowIndex + 1;
326         }
327 
328         if (mAdapter != null) {
329             DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false)
330                     .dispatchUpdatesTo(mAdapter);
331         }
332     }
333 
addPrivateSpaceItems(int position)334     int addPrivateSpaceItems(int position) {
335         if (mPrivateProviderManager != null
336                 && !mPrivateProviderManager.isPrivateSpaceHidden()
337                 && !mPrivateApps.isEmpty()) {
338             // Always add PS Header if Space is present and visible.
339             position = mPrivateProviderManager.addPrivateSpaceHeader(mAdapterItems);
340             mFastScrollerSections.add(new FastScrollSectionInfo(
341                     mPrivateProfileAppScrollerBadge, position));
342             int privateSpaceState = mPrivateProviderManager.getCurrentState();
343             switch (privateSpaceState) {
344                 case PrivateProfileManager.STATE_DISABLED:
345                 case PrivateProfileManager.STATE_TRANSITION:
346                     break;
347                 case PrivateProfileManager.STATE_ENABLED:
348                     // Add PS Apps only in Enabled State.
349                     position = addPrivateSpaceApps(position);
350                     break;
351             }
352         }
353         return position;
354     }
355 
addPrivateSpaceApps(int position)356     private int addPrivateSpaceApps(int position) {
357         // Add Install Apps Button first.
358         if (Flags.privateSpaceAppInstallerButton()) {
359             mPrivateProviderManager.addPrivateSpaceInstallAppButton(mAdapterItems);
360             position++;
361         }
362 
363         // Split of private space apps into user-installed and system apps.
364         Map<Boolean, List<AppInfo>> split = mPrivateApps.stream()
365                 .collect(Collectors.partitioningBy(mPrivateProviderManager
366                                 .splitIntoUserInstalledAndSystemApps(mActivityContext)));
367 
368         // TODO(b/329688630): switch to the pulled LayoutStaticSnapshot atom
369         mActivityContext
370                 .getStatsLogManager()
371                 .logger()
372                 .withCardinality(split.get(true).size())
373                 .log(LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT);
374 
375         mActivityContext
376                 .getStatsLogManager()
377                 .logger()
378                 .withCardinality(split.get(false).size())
379                 .log(LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT);
380 
381         // Add user installed apps
382         position = addAppsWithSections(split.get(true), position);
383         // Add system apps separator.
384         if (Flags.privateSpaceSysAppsSeparation()) {
385             position = mPrivateProviderManager.addSystemAppsDivider(mAdapterItems);
386         }
387         // Add system apps.
388         position = addAppsWithSections(split.get(false), position);
389 
390         return position;
391     }
392 
addAppsWithSections(List<AppInfo> appList, int startPosition)393     private int addAppsWithSections(List<AppInfo> appList, int startPosition) {
394         String lastSectionName = null;
395         boolean hasPrivateApps = false;
396         int position = startPosition;
397         if (mPrivateProviderManager != null) {
398             hasPrivateApps = appList.stream().
399                     allMatch(mPrivateProviderManager.getItemInfoMatcher());
400         }
401         for (int i = 0; i < appList.size(); i++) {
402             AppInfo info = appList.get(i);
403             // Apply decorator to private apps.
404             if (hasPrivateApps) {
405                 mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
406                         new SectionDecorationInfo(mActivityContext.getApplicationContext(),
407                                 getRoundRegions(i, appList.size()),
408                                 true /* decorateTogether */)));
409             } else {
410                 mAdapterItems.add(AdapterItem.asApp(info));
411             }
412 
413             String sectionName = info.sectionName;
414             // Create a new section if the section names do not match
415             if (!sectionName.equals(lastSectionName)) {
416                 lastSectionName = sectionName;
417                 mFastScrollerSections.add(new FastScrollSectionInfo(hasPrivateApps ?
418                         mPrivateProfileAppScrollerBadge : sectionName, position));
419             }
420             position++;
421         }
422         return position;
423     }
424 
425     /**
426      * Determines the corner regions that should be rounded for a specific app icon based on its
427      * position in a grid. Apps that should only be cared about rounding are the apps in the last
428      * row. In the last row on the first column, the app should only be rounded on the bottom left.
429      * Apps in the middle would not be rounded and the last app on the last row will ALWAYS have a
430      * {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}.
431      *
432      * @param appIndex The index of the app icon within the app list.
433      * @param appListSize The total number of apps within the app list.
434      * @return  An integer representing the corner regions to be rounded, using bitwise flags:
435      *          - {@link SectionDecorationInfo#ROUND_NOTHING}: No corners should be rounded.
436      *          - {@link SectionDecorationInfo#ROUND_TOP_LEFT}: Round the top-left corner.
437      *          - {@link SectionDecorationInfo#ROUND_TOP_RIGHT}: Round the top-right corner.
438      *          - {@link SectionDecorationInfo#ROUND_BOTTOM_LEFT}: Round the bottom-left corner.
439      *          - {@link SectionDecorationInfo#ROUND_BOTTOM_RIGHT}: Round the bottom-right corner.
440      */
441     @VisibleForTesting
getRoundRegions(int appIndex, int appListSize)442     int getRoundRegions(int appIndex, int appListSize) {
443         int numberOfAppRows = (int) Math.ceil((double) appListSize / mNumAppsPerRowAllApps);
444         int roundRegion = ROUND_NOTHING;
445         // App is in the last row.
446         if ((appIndex / mNumAppsPerRowAllApps) == numberOfAppRows - 1) {
447             if ((appIndex % mNumAppsPerRowAllApps) == 0) {
448                 // App is the first column.
449                 roundRegion = ROUND_BOTTOM_LEFT;
450             } else if ((appIndex % mNumAppsPerRowAllApps) == mNumAppsPerRowAllApps-1) {
451                 // App is in the last column.
452                 roundRegion = ROUND_BOTTOM_RIGHT;
453             }
454             // Ensure the last private app is rounded on the bottom right.
455             if (appIndex == appListSize - 1) {
456                 roundRegion |= ROUND_BOTTOM_RIGHT;
457             }
458         }
459         return roundRegion;
460     }
461 
getPrivateProfileManager()462     public PrivateProfileManager getPrivateProfileManager() {
463         return mPrivateProviderManager;
464     }
465 
466     private static class MyDiffCallback extends DiffUtil.Callback {
467 
468         private final List<AdapterItem> mOldList;
469         private final List<AdapterItem> mNewList;
470 
MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList)471         MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList) {
472             mOldList = oldList;
473             mNewList = newList;
474         }
475 
476         @Override
getOldListSize()477         public int getOldListSize() {
478             return mOldList.size();
479         }
480 
481         @Override
getNewListSize()482         public int getNewListSize() {
483             return mNewList.size();
484         }
485 
486         @Override
areItemsTheSame(int oldItemPosition, int newItemPosition)487         public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
488             return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition));
489         }
490 
491         @Override
areContentsTheSame(int oldItemPosition, int newItemPosition)492         public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
493             return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition));
494         }
495     }
496 
497 }