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.support.v7.widget.RecyclerView;
     20 import android.util.Log;
     21 import com.android.launcher3.AppInfo;
     22 import com.android.launcher3.Launcher;
     23 import com.android.launcher3.LauncherAppState;
     24 import com.android.launcher3.compat.AlphabeticIndexCompat;
     25 import com.android.launcher3.compat.UserHandleCompat;
     26 import com.android.launcher3.model.AppNameComparator;
     27 import com.android.launcher3.util.ComponentKey;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Collections;
     31 import java.util.HashMap;
     32 import java.util.List;
     33 import java.util.Locale;
     34 import java.util.Map;
     35 import java.util.TreeMap;
     36 
     37 /**
     38  * The alphabetically sorted list of applications.
     39  */
     40 public class AlphabeticalAppsList {
     41 
     42     public static final String TAG = "AlphabeticalAppsList";
     43     private static final boolean DEBUG = false;
     44     private static final boolean DEBUG_PREDICTIONS = false;
     45 
     46     /**
     47      * Info about a section in the alphabetic list
     48      */
     49     public static class SectionInfo {
     50         // The number of applications in this section
     51         public int numApps;
     52         // The section break AdapterItem for this section
     53         public AdapterItem sectionBreakItem;
     54         // The first app AdapterItem for this section
     55         public AdapterItem firstAppItem;
     56     }
     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         // The row that this item shows up on
     85         public int rowIndex;
     86 
     87         /** Section & App properties */
     88         // The section for this item
     89         public SectionInfo sectionInfo;
     90 
     91         /** App-only properties */
     92         // The section name of this app.  Note that there can be multiple items with different
     93         // sectionNames in the same section
     94         public String sectionName = null;
     95         // The index of this app in the section
     96         public int sectionAppIndex = -1;
     97         // The index of this app in the row
     98         public int rowAppIndex;
     99         // The associated AppInfo for the app
    100         public AppInfo appInfo = null;
    101         // The index of this app not including sections
    102         public int appIndex = -1;
    103 
    104         public static AdapterItem asSectionBreak(int pos, SectionInfo section) {
    105             AdapterItem item = new AdapterItem();
    106             item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE;
    107             item.position = pos;
    108             item.sectionInfo = section;
    109             section.sectionBreakItem = item;
    110             return item;
    111         }
    112 
    113         public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName,
    114                                         int sectionAppIndex, AppInfo appInfo, int appIndex) {
    115             AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex);
    116             item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
    117             return item;
    118         }
    119 
    120         public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
    121                                         int sectionAppIndex, AppInfo appInfo, int appIndex) {
    122             AdapterItem item = new AdapterItem();
    123             item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE;
    124             item.position = pos;
    125             item.sectionInfo = section;
    126             item.sectionName = sectionName;
    127             item.sectionAppIndex = sectionAppIndex;
    128             item.appInfo = appInfo;
    129             item.appIndex = appIndex;
    130             return item;
    131         }
    132     }
    133 
    134     /**
    135      * Common interface for different merging strategies.
    136      */
    137     public interface MergeAlgorithm {
    138         boolean continueMerging(SectionInfo section, SectionInfo withSection,
    139                 int sectionAppCount, int numAppsPerRow, int mergeCount);
    140     }
    141 
    142     private Launcher mLauncher;
    143 
    144     // The set of apps from the system not including predictions
    145     private final List<AppInfo> mApps = new ArrayList<>();
    146     private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
    147 
    148     // The set of filtered apps with the current filter
    149     private List<AppInfo> mFilteredApps = new ArrayList<>();
    150     // The current set of adapter items
    151     private List<AdapterItem> mAdapterItems = new ArrayList<>();
    152     // The set of sections for the apps with the current filter
    153     private List<SectionInfo> mSections = new ArrayList<>();
    154     // The set of sections that we allow fast-scrolling to (includes non-merged sections)
    155     private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
    156     // The set of predicted app component names
    157     private List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
    158     // The set of predicted apps resolved from the component names and the current set of apps
    159     private List<AppInfo> mPredictedApps = new ArrayList<>();
    160     // The of ordered component names as a result of a search query
    161     private ArrayList<ComponentKey> mSearchResults;
    162     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
    163     private RecyclerView.Adapter mAdapter;
    164     private AlphabeticIndexCompat mIndexer;
    165     private AppNameComparator mAppNameComparator;
    166     private MergeAlgorithm mMergeAlgorithm;
    167     private int mNumAppsPerRow;
    168     private int mNumPredictedAppsPerRow;
    169     private int mNumAppRowsInAdapter;
    170 
    171     public AlphabeticalAppsList(Context context) {
    172         mLauncher = (Launcher) context;
    173         mIndexer = new AlphabeticIndexCompat(context);
    174         mAppNameComparator = new AppNameComparator(context);
    175     }
    176 
    177     /**
    178      * Sets the number of apps per row.
    179      */
    180     public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow,
    181             MergeAlgorithm mergeAlgorithm) {
    182         mNumAppsPerRow = numAppsPerRow;
    183         mNumPredictedAppsPerRow = numPredictedAppsPerRow;
    184         mMergeAlgorithm = mergeAlgorithm;
    185 
    186         updateAdapterItems();
    187     }
    188 
    189     /**
    190      * Sets the adapter to notify when this dataset changes.
    191      */
    192     public void setAdapter(RecyclerView.Adapter adapter) {
    193         mAdapter = adapter;
    194     }
    195 
    196     /**
    197      * Returns all the apps.
    198      */
    199     public List<AppInfo> getApps() {
    200         return mApps;
    201     }
    202 
    203     /**
    204      * Returns sections of all the current filtered applications.
    205      */
    206     public List<SectionInfo> getSections() {
    207         return mSections;
    208     }
    209 
    210     /**
    211      * Returns fast scroller sections of all the current filtered applications.
    212      */
    213     public List<FastScrollSectionInfo> getFastScrollerSections() {
    214         return mFastScrollerSections;
    215     }
    216 
    217     /**
    218      * Returns the current filtered list of applications broken down into their sections.
    219      */
    220     public List<AdapterItem> getAdapterItems() {
    221         return mAdapterItems;
    222     }
    223 
    224     /**
    225      * Returns the number of applications in this list.
    226      */
    227     public int getSize() {
    228         return mFilteredApps.size();
    229     }
    230 
    231     /**
    232      * Returns the number of rows of applications (not including predictions)
    233      */
    234     public int getNumAppRows() {
    235         return mNumAppRowsInAdapter;
    236     }
    237 
    238     /**
    239      * Returns whether there are is a filter set.
    240      */
    241     public boolean hasFilter() {
    242         return (mSearchResults != null);
    243     }
    244 
    245     /**
    246      * Returns whether there are no filtered results.
    247      */
    248     public boolean hasNoFilteredResults() {
    249         return (mSearchResults != null) && mFilteredApps.isEmpty();
    250     }
    251 
    252     /**
    253      * Sets the sorted list of filtered components.
    254      */
    255     public void setOrderedFilter(ArrayList<ComponentKey> f) {
    256         if (mSearchResults != f) {
    257             mSearchResults = f;
    258             updateAdapterItems();
    259         }
    260     }
    261 
    262     /**
    263      * Sets the current set of predicted apps.  Since this can be called before we get the full set
    264      * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
    265      */
    266     public void setPredictedApps(List<ComponentKey> apps) {
    267         mPredictedAppComponents.clear();
    268         mPredictedAppComponents.addAll(apps);
    269         onAppsUpdated();
    270     }
    271 
    272     /**
    273      * Sets the current set of apps.
    274      */
    275     public void setApps(List<AppInfo> apps) {
    276         mComponentToAppMap.clear();
    277         addApps(apps);
    278     }
    279 
    280     /**
    281      * Adds new apps to the list.
    282      */
    283     public void addApps(List<AppInfo> apps) {
    284         updateApps(apps);
    285     }
    286 
    287     /**
    288      * Updates existing apps in the list
    289      */
    290     public void updateApps(List<AppInfo> apps) {
    291         for (AppInfo app : apps) {
    292             mComponentToAppMap.put(app.toComponentKey(), app);
    293         }
    294         onAppsUpdated();
    295     }
    296 
    297     /**
    298      * Removes some apps from the list.
    299      */
    300     public void removeApps(List<AppInfo> apps) {
    301         for (AppInfo app : apps) {
    302             mComponentToAppMap.remove(app.toComponentKey());
    303         }
    304         onAppsUpdated();
    305     }
    306 
    307     /**
    308      * Updates internals when the set of apps are updated.
    309      */
    310     private void onAppsUpdated() {
    311         // Sort the list of apps
    312         mApps.clear();
    313         mApps.addAll(mComponentToAppMap.values());
    314         Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
    315 
    316         // As a special case for some languages (currently only Simplified Chinese), we may need to
    317         // coalesce sections
    318         Locale curLocale = mLauncher.getResources().getConfiguration().locale;
    319         TreeMap<String, ArrayList<AppInfo>> sectionMap = null;
    320         boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
    321         if (localeRequiresSectionSorting) {
    322             // Compute the section headers.  We use a TreeMap with the section name comparator to
    323             // ensure that the sections are ordered when we iterate over it later
    324             sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator());
    325             for (AppInfo info : mApps) {
    326                 // Add the section to the cache
    327                 String sectionName = getAndUpdateCachedSectionName(info.title);
    328 
    329                 // Add it to the mapping
    330                 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
    331                 if (sectionApps == null) {
    332                     sectionApps = new ArrayList<>();
    333                     sectionMap.put(sectionName, sectionApps);
    334                 }
    335                 sectionApps.add(info);
    336             }
    337 
    338             // Add each of the section apps to the list in order
    339             List<AppInfo> allApps = new ArrayList<>(mApps.size());
    340             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
    341                 allApps.addAll(entry.getValue());
    342             }
    343 
    344             mApps.clear();
    345             mApps.addAll(allApps);
    346         } else {
    347             // Just compute the section headers for use below
    348             for (AppInfo info : mApps) {
    349                 // Add the section to the cache
    350                 getAndUpdateCachedSectionName(info.title);
    351             }
    352         }
    353 
    354         // Recompose the set of adapter items from the current set of apps
    355         updateAdapterItems();
    356     }
    357 
    358     /**
    359      * Updates the set of filtered apps with the current filter.  At this point, we expect
    360      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
    361      */
    362     private void updateAdapterItems() {
    363         SectionInfo lastSectionInfo = null;
    364         String lastSectionName = null;
    365         FastScrollSectionInfo lastFastScrollerSectionInfo = null;
    366         int position = 0;
    367         int appIndex = 0;
    368 
    369         // Prepare to update the list of sections, filtered apps, etc.
    370         mFilteredApps.clear();
    371         mFastScrollerSections.clear();
    372         mAdapterItems.clear();
    373         mSections.clear();
    374 
    375         if (DEBUG_PREDICTIONS) {
    376             if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
    377                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
    378                         UserHandleCompat.myUserHandle()));
    379                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
    380                         UserHandleCompat.myUserHandle()));
    381                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
    382                         UserHandleCompat.myUserHandle()));
    383                 mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
    384                         UserHandleCompat.myUserHandle()));
    385             }
    386         }
    387 
    388         // Process the predicted app components
    389         mPredictedApps.clear();
    390         if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
    391             for (ComponentKey ck : mPredictedAppComponents) {
    392                 AppInfo info = mComponentToAppMap.get(ck);
    393                 if (info != null) {
    394                     mPredictedApps.add(info);
    395                 } else {
    396                     if (LauncherAppState.isDogfoodBuild()) {
    397                         Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher));
    398                     }
    399                 }
    400                 // Stop at the number of predicted apps
    401                 if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
    402                     break;
    403                 }
    404             }
    405 
    406             if (!mPredictedApps.isEmpty()) {
    407                 // Add a section for the predictions
    408                 lastSectionInfo = new SectionInfo();
    409                 lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
    410                 AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
    411                 mSections.add(lastSectionInfo);
    412                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
    413                 mAdapterItems.add(sectionItem);
    414 
    415                 // Add the predicted app items
    416                 for (AppInfo info : mPredictedApps) {
    417                     AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo,
    418                             "", lastSectionInfo.numApps++, info, appIndex++);
    419                     if (lastSectionInfo.firstAppItem == null) {
    420                         lastSectionInfo.firstAppItem = appItem;
    421                         lastFastScrollerSectionInfo.fastScrollToItem = appItem;
    422                     }
    423                     mAdapterItems.add(appItem);
    424                     mFilteredApps.add(info);
    425                 }
    426             }
    427         }
    428 
    429         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
    430         // ordered set of sections
    431         for (AppInfo info : getFiltersAppInfos()) {
    432             String sectionName = getAndUpdateCachedSectionName(info.title);
    433 
    434             // Create a new section if the section names do not match
    435             if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
    436                 lastSectionName = sectionName;
    437                 lastSectionInfo = new SectionInfo();
    438                 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
    439                 mSections.add(lastSectionInfo);
    440                 mFastScrollerSections.add(lastFastScrollerSectionInfo);
    441 
    442                 // Create a new section item to break the flow of items in the list
    443                 if (!hasFilter()) {
    444                     AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
    445                     mAdapterItems.add(sectionItem);
    446                 }
    447             }
    448 
    449             // Create an app item
    450             AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
    451                     lastSectionInfo.numApps++, info, appIndex++);
    452             if (lastSectionInfo.firstAppItem == null) {
    453                 lastSectionInfo.firstAppItem = appItem;
    454                 lastFastScrollerSectionInfo.fastScrollToItem = appItem;
    455             }
    456             mAdapterItems.add(appItem);
    457             mFilteredApps.add(info);
    458         }
    459 
    460         // Merge multiple sections together as requested by the merge strategy for this device
    461         mergeSections();
    462 
    463         if (mNumAppsPerRow != 0) {
    464             // Update the number of rows in the adapter after we do all the merging (otherwise, we
    465             // would have to shift the values again)
    466             int numAppsInSection = 0;
    467             int numAppsInRow = 0;
    468             int rowIndex = -1;
    469             for (AdapterItem item : mAdapterItems) {
    470                 item.rowIndex = 0;
    471                 if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) {
    472                     numAppsInSection = 0;
    473                 } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
    474                         item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
    475                     if (numAppsInSection % mNumAppsPerRow == 0) {
    476                         numAppsInRow = 0;
    477                         rowIndex++;
    478                     }
    479                     item.rowIndex = rowIndex;
    480                     item.rowAppIndex = numAppsInRow;
    481                     numAppsInSection++;
    482                     numAppsInRow++;
    483                 }
    484             }
    485             mNumAppRowsInAdapter = rowIndex + 1;
    486 
    487             // Pre-calculate all the fast scroller fractions based on the number of rows
    488             float rowFraction = 1f / mNumAppRowsInAdapter;
    489             for (FastScrollSectionInfo info : mFastScrollerSections) {
    490                 AdapterItem item = info.fastScrollToItem;
    491                 if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
    492                         item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
    493                     info.touchFraction = 0f;
    494                     continue;
    495                 }
    496 
    497                 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
    498                 info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
    499             }
    500         }
    501 
    502         // Refresh the recycler view
    503         if (mAdapter != null) {
    504             mAdapter.notifyDataSetChanged();
    505         }
    506     }
    507 
    508     private List<AppInfo> getFiltersAppInfos() {
    509         if (mSearchResults == null) {
    510             return mApps;
    511         }
    512 
    513         ArrayList<AppInfo> result = new ArrayList<>();
    514         for (ComponentKey key : mSearchResults) {
    515             AppInfo match = mComponentToAppMap.get(key);
    516             if (match != null) {
    517                 result.add(match);
    518             }
    519         }
    520         return result;
    521     }
    522 
    523     /**
    524      * Merges multiple sections to reduce visual raggedness.
    525      */
    526     private void mergeSections() {
    527         // Ignore merging until we have an algorithm and a valid row size
    528         if (mMergeAlgorithm == null || mNumAppsPerRow == 0) {
    529             return;
    530         }
    531 
    532         // Go through each section and try and merge some of the sections
    533         if (!hasFilter()) {
    534             int sectionAppCount = 0;
    535             for (int i = 0; i < mSections.size() - 1; i++) {
    536                 SectionInfo section = mSections.get(i);
    537                 sectionAppCount = section.numApps;
    538                 int mergeCount = 1;
    539 
    540                 // Merge rows based on the current strategy
    541                 while (i < (mSections.size() - 1) &&
    542                         mMergeAlgorithm.continueMerging(section, mSections.get(i + 1),
    543                                 sectionAppCount, mNumAppsPerRow, mergeCount)) {
    544                     SectionInfo nextSection = mSections.remove(i + 1);
    545 
    546                     // Remove the next section break
    547                     mAdapterItems.remove(nextSection.sectionBreakItem);
    548                     int pos = mAdapterItems.indexOf(section.firstAppItem);
    549 
    550                     // Point the section for these new apps to the merged section
    551                     int nextPos = pos + section.numApps;
    552                     for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) {
    553                         AdapterItem item = mAdapterItems.get(j);
    554                         item.sectionInfo = section;
    555                         item.sectionAppIndex += section.numApps;
    556                     }
    557 
    558                     // Update the following adapter items of the removed section item
    559                     pos = mAdapterItems.indexOf(nextSection.firstAppItem);
    560                     for (int j = pos; j < mAdapterItems.size(); j++) {
    561                         AdapterItem item = mAdapterItems.get(j);
    562                         item.position--;
    563                     }
    564                     section.numApps += nextSection.numApps;
    565                     sectionAppCount += nextSection.numApps;
    566 
    567                     if (DEBUG) {
    568                         Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName +
    569                                 " to " + section.firstAppItem.sectionName +
    570                                 " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow));
    571                     }
    572                     mergeCount++;
    573                 }
    574             }
    575         }
    576     }
    577 
    578     /**
    579      * Returns the cached section name for the given title, recomputing and updating the cache if
    580      * the title has no cached section name.
    581      */
    582     private String getAndUpdateCachedSectionName(CharSequence title) {
    583         String sectionName = mCachedSectionNames.get(title);
    584         if (sectionName == null) {
    585             sectionName = mIndexer.computeSectionName(title);
    586             mCachedSectionNames.put(title, sectionName);
    587         }
    588         return sectionName;
    589     }
    590 }
    591