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