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.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 
     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 
     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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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      */
    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      */
    217     public void setAdapter(AllAppsGridAdapter adapter) {
    218         mAdapter = adapter;
    219     }
    220 
    221     /**
    222      * Returns all the apps.
    223      */
    224     public List<AppInfo> getApps() {
    225         return mApps;
    226     }
    227 
    228     /**
    229      * Returns fast scroller sections of all the current filtered applications.
    230      */
    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      */
    238     public List<AdapterItem> getAdapterItems() {
    239         return mAdapterItems;
    240     }
    241 
    242     /**
    243      * Returns the number of rows of applications (not including predictions)
    244      */
    245     public int getNumAppRows() {
    246         return mNumAppRowsInAdapter;
    247     }
    248 
    249     /**
    250      * Returns the number of applications in this list.
    251      */
    252     public int getNumFilteredApps() {
    253         return mFilteredApps.size();
    254     }
    255 
    256     /**
    257      * Returns whether there are is a filter set.
    258      */
    259     public boolean hasFilter() {
    260         return (mSearchResults != null);
    261     }
    262 
    263     /**
    264      * Returns whether there are no filtered results.
    265      */
    266     public boolean hasNoFilteredResults() {
    267         return (mSearchResults != null) && mFilteredApps.isEmpty();
    268     }
    269 
    270     boolean shouldShowEmptySearch() {
    271         return hasNoFilteredResults() && !isAppDiscoveryRunning() && mDiscoveredApps.isEmpty();
    272     }
    273 
    274     /**
    275      * Sets the sorted list of filtered components.
    276      */
    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 
    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      */
    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      */
    314     public void setApps(List<AppInfo> apps) {
    315         mComponentToAppMap.clear();
    316         addApps(apps);
    317     }
    318 
    319     /**
    320      * Adds new apps to the list.
    321      */
    322     public void addApps(List<AppInfo> apps) {
    323         updateApps(apps);
    324     }
    325 
    326     /**
    327      * Updates existing apps in the list
    328      */
    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      */
    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      */
    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      */
    397     private void updateAdapterItems() {
    398         refillAdapterItems();
    399         refreshRecyclerView();
    400     }
    401 
    402     private void refreshRecyclerView() {
    403         if (mAdapter != null) {
    404             mAdapter.notifyDataSetChanged();
    405         }
    406     }
    407 
    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 
    578     public boolean isAppDiscoveryRunning() {
    579         return mAppDiscoveryUpdateState == AppDiscoveryUpdateState.START
    580                 || mAppDiscoveryUpdateState == AppDiscoveryUpdateState.UPDATE;
    581     }
    582 
    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      */
    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