summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/launcher3/allapps/AlphabeticalAppsList.java')
-rw-r--r--src/com/android/launcher3/allapps/AlphabeticalAppsList.java590
1 files changed, 590 insertions, 0 deletions
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
new file mode 100644
index 000000000..47241ce5d
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.model.AppNameComparator;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * The alphabetically sorted list of applications.
+ */
+public class AlphabeticalAppsList {
+
+ public static final String TAG = "AlphabeticalAppsList";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_PREDICTIONS = false;
+
+ /**
+ * Info about a section in the alphabetic list
+ */
+ public static class SectionInfo {
+ // The number of applications in this section
+ public int numApps;
+ // The section break AdapterItem for this section
+ public AdapterItem sectionBreakItem;
+ // The first app AdapterItem for this section
+ public AdapterItem firstAppItem;
+ }
+
+ /**
+ * Info about a fast scroller section, depending if sections are merged, the fast scroller
+ * sections will not be the same set as the section headers.
+ */
+ public static class FastScrollSectionInfo {
+ // The section name
+ public String sectionName;
+ // The AdapterItem to scroll to for this section
+ public AdapterItem fastScrollToItem;
+ // The touch fraction that should map to this fast scroll section info
+ public float touchFraction;
+
+ public FastScrollSectionInfo(String sectionName) {
+ this.sectionName = sectionName;
+ }
+ }
+
+ /**
+ * Info about a particular adapter item (can be either section or app)
+ */
+ public static class AdapterItem {
+ /** Common properties */
+ // The index of this adapter item in the list
+ public int position;
+ // The type of this item
+ public int viewType;
+ // The row that this item shows up on
+ public int rowIndex;
+
+ /** Section & App properties */
+ // The section for this item
+ public SectionInfo sectionInfo;
+
+ /** App-only properties */
+ // The section name of this app. Note that there can be multiple items with different
+ // sectionNames in the same section
+ public String sectionName = null;
+ // The index of this app in the section
+ public int sectionAppIndex = -1;
+ // The index of this app in the row
+ public int rowAppIndex;
+ // The associated AppInfo for the app
+ public AppInfo appInfo = null;
+ // The index of this app not including sections
+ public int appIndex = -1;
+
+ public static AdapterItem asSectionBreak(int pos, SectionInfo section) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE;
+ item.position = pos;
+ item.sectionInfo = section;
+ section.sectionBreakItem = item;
+ return item;
+ }
+
+ public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName,
+ int sectionAppIndex, AppInfo appInfo, int appIndex) {
+ AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex);
+ item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
+ return item;
+ }
+
+ public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
+ int sectionAppIndex, AppInfo appInfo, int appIndex) {
+ AdapterItem item = new AdapterItem();
+ item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE;
+ item.position = pos;
+ item.sectionInfo = section;
+ item.sectionName = sectionName;
+ item.sectionAppIndex = sectionAppIndex;
+ item.appInfo = appInfo;
+ item.appIndex = appIndex;
+ return item;
+ }
+ }
+
+ /**
+ * Common interface for different merging strategies.
+ */
+ public interface MergeAlgorithm {
+ boolean continueMerging(SectionInfo section, SectionInfo withSection,
+ int sectionAppCount, int numAppsPerRow, int mergeCount);
+ }
+
+ private Launcher mLauncher;
+
+ // The set of apps from the system not including predictions
+ private final List<AppInfo> mApps = new ArrayList<>();
+ private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
+
+ // The set of filtered apps with the current filter
+ private List<AppInfo> mFilteredApps = new ArrayList<>();
+ // The current set of adapter items
+ private List<AdapterItem> mAdapterItems = new ArrayList<>();
+ // The set of sections for the apps with the current filter
+ private List<SectionInfo> mSections = new ArrayList<>();
+ // The set of sections that we allow fast-scrolling to (includes non-merged sections)
+ private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
+ // The set of predicted app component names
+ private List<ComponentKey> mPredictedAppComponents = new ArrayList<>();
+ // The set of predicted apps resolved from the component names and the current set of apps
+ private List<AppInfo> mPredictedApps = new ArrayList<>();
+ // The of ordered component names as a result of a search query
+ private ArrayList<ComponentKey> mSearchResults;
+ private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
+ private RecyclerView.Adapter mAdapter;
+ private AlphabeticIndexCompat mIndexer;
+ private AppNameComparator mAppNameComparator;
+ private MergeAlgorithm mMergeAlgorithm;
+ private int mNumAppsPerRow;
+ private int mNumPredictedAppsPerRow;
+ private int mNumAppRowsInAdapter;
+
+ public AlphabeticalAppsList(Context context) {
+ mLauncher = (Launcher) context;
+ mIndexer = new AlphabeticIndexCompat(context);
+ mAppNameComparator = new AppNameComparator(context);
+ }
+
+ /**
+ * Sets the number of apps per row.
+ */
+ public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow,
+ MergeAlgorithm mergeAlgorithm) {
+ mNumAppsPerRow = numAppsPerRow;
+ mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+ mMergeAlgorithm = mergeAlgorithm;
+
+ updateAdapterItems();
+ }
+
+ /**
+ * Sets the adapter to notify when this dataset changes.
+ */
+ public void setAdapter(RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /**
+ * Returns all the apps.
+ */
+ public List<AppInfo> getApps() {
+ return mApps;
+ }
+
+ /**
+ * Returns sections of all the current filtered applications.
+ */
+ public List<SectionInfo> getSections() {
+ return mSections;
+ }
+
+ /**
+ * Returns fast scroller sections of all the current filtered applications.
+ */
+ public List<FastScrollSectionInfo> getFastScrollerSections() {
+ return mFastScrollerSections;
+ }
+
+ /**
+ * Returns the current filtered list of applications broken down into their sections.
+ */
+ public List<AdapterItem> getAdapterItems() {
+ return mAdapterItems;
+ }
+
+ /**
+ * Returns the number of applications in this list.
+ */
+ public int getSize() {
+ return mFilteredApps.size();
+ }
+
+ /**
+ * Returns the number of rows of applications (not including predictions)
+ */
+ public int getNumAppRows() {
+ return mNumAppRowsInAdapter;
+ }
+
+ /**
+ * Returns whether there are is a filter set.
+ */
+ public boolean hasFilter() {
+ return (mSearchResults != null);
+ }
+
+ /**
+ * Returns whether there are no filtered results.
+ */
+ public boolean hasNoFilteredResults() {
+ return (mSearchResults != null) && mFilteredApps.isEmpty();
+ }
+
+ /**
+ * Sets the sorted list of filtered components.
+ */
+ public void setOrderedFilter(ArrayList<ComponentKey> f) {
+ if (mSearchResults != f) {
+ mSearchResults = f;
+ updateAdapterItems();
+ }
+ }
+
+ /**
+ * Sets the current set of predicted apps. Since this can be called before we get the full set
+ * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
+ */
+ public void setPredictedApps(List<ComponentKey> apps) {
+ mPredictedAppComponents.clear();
+ mPredictedAppComponents.addAll(apps);
+ onAppsUpdated();
+ }
+
+ /**
+ * Sets the current set of apps.
+ */
+ public void setApps(List<AppInfo> apps) {
+ mComponentToAppMap.clear();
+ addApps(apps);
+ }
+
+ /**
+ * Adds new apps to the list.
+ */
+ public void addApps(List<AppInfo> apps) {
+ updateApps(apps);
+ }
+
+ /**
+ * Updates existing apps in the list
+ */
+ public void updateApps(List<AppInfo> apps) {
+ for (AppInfo app : apps) {
+ mComponentToAppMap.put(app.toComponentKey(), app);
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Removes some apps from the list.
+ */
+ public void removeApps(List<AppInfo> apps) {
+ for (AppInfo app : apps) {
+ mComponentToAppMap.remove(app.toComponentKey());
+ }
+ onAppsUpdated();
+ }
+
+ /**
+ * Updates internals when the set of apps are updated.
+ */
+ private void onAppsUpdated() {
+ // Sort the list of apps
+ mApps.clear();
+ mApps.addAll(mComponentToAppMap.values());
+ Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
+
+ // As a special case for some languages (currently only Simplified Chinese), we may need to
+ // coalesce sections
+ Locale curLocale = mLauncher.getResources().getConfiguration().locale;
+ TreeMap<String, ArrayList<AppInfo>> sectionMap = null;
+ boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE);
+ if (localeRequiresSectionSorting) {
+ // Compute the section headers. We use a TreeMap with the section name comparator to
+ // ensure that the sections are ordered when we iterate over it later
+ sectionMap = new TreeMap<>(mAppNameComparator.getSectionNameComparator());
+ for (AppInfo info : mApps) {
+ // Add the section to the cache
+ String sectionName = getAndUpdateCachedSectionName(info.title);
+
+ // Add it to the mapping
+ ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName);
+ if (sectionApps == null) {
+ sectionApps = new ArrayList<>();
+ sectionMap.put(sectionName, sectionApps);
+ }
+ sectionApps.add(info);
+ }
+
+ // Add each of the section apps to the list in order
+ List<AppInfo> allApps = new ArrayList<>(mApps.size());
+ for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
+ allApps.addAll(entry.getValue());
+ }
+
+ mApps.clear();
+ mApps.addAll(allApps);
+ } else {
+ // Just compute the section headers for use below
+ for (AppInfo info : mApps) {
+ // Add the section to the cache
+ getAndUpdateCachedSectionName(info.title);
+ }
+ }
+
+ // Recompose the set of adapter items from the current set of apps
+ updateAdapterItems();
+ }
+
+ /**
+ * Updates the set of filtered apps with the current filter. At this point, we expect
+ * mCachedSectionNames to have been calculated for the set of all apps in mApps.
+ */
+ private void updateAdapterItems() {
+ SectionInfo lastSectionInfo = null;
+ String lastSectionName = null;
+ FastScrollSectionInfo lastFastScrollerSectionInfo = null;
+ int position = 0;
+ int appIndex = 0;
+
+ // Prepare to update the list of sections, filtered apps, etc.
+ mFilteredApps.clear();
+ mFastScrollerSections.clear();
+ mAdapterItems.clear();
+ mSections.clear();
+
+ if (DEBUG_PREDICTIONS) {
+ if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ mPredictedAppComponents.add(new ComponentKey(mApps.get(0).componentName,
+ UserHandleCompat.myUserHandle()));
+ }
+ }
+
+ // Process the predicted app components
+ mPredictedApps.clear();
+ if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
+ for (ComponentKey ck : mPredictedAppComponents) {
+ AppInfo info = mComponentToAppMap.get(ck);
+ if (info != null) {
+ mPredictedApps.add(info);
+ } else {
+ if (LauncherAppState.isDogfoodBuild()) {
+ Log.e(TAG, "Predicted app not found: " + ck.flattenToString(mLauncher));
+ }
+ }
+ // Stop at the number of predicted apps
+ if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
+ break;
+ }
+ }
+
+ if (!mPredictedApps.isEmpty()) {
+ // Add a section for the predictions
+ lastSectionInfo = new SectionInfo();
+ lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
+ AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
+ mSections.add(lastSectionInfo);
+ mFastScrollerSections.add(lastFastScrollerSectionInfo);
+ mAdapterItems.add(sectionItem);
+
+ // Add the predicted app items
+ for (AppInfo info : mPredictedApps) {
+ AdapterItem appItem = AdapterItem.asPredictedApp(position++, lastSectionInfo,
+ "", lastSectionInfo.numApps++, info, appIndex++);
+ if (lastSectionInfo.firstAppItem == null) {
+ lastSectionInfo.firstAppItem = appItem;
+ lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ }
+ mAdapterItems.add(appItem);
+ mFilteredApps.add(info);
+ }
+ }
+ }
+
+ // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
+ // ordered set of sections
+ for (AppInfo info : getFiltersAppInfos()) {
+ String sectionName = getAndUpdateCachedSectionName(info.title);
+
+ // Create a new section if the section names do not match
+ if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
+ lastSectionName = sectionName;
+ lastSectionInfo = new SectionInfo();
+ lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
+ mSections.add(lastSectionInfo);
+ mFastScrollerSections.add(lastFastScrollerSectionInfo);
+
+ // Create a new section item to break the flow of items in the list
+ if (!hasFilter()) {
+ AdapterItem sectionItem = AdapterItem.asSectionBreak(position++, lastSectionInfo);
+ mAdapterItems.add(sectionItem);
+ }
+ }
+
+ // Create an app item
+ AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
+ lastSectionInfo.numApps++, info, appIndex++);
+ if (lastSectionInfo.firstAppItem == null) {
+ lastSectionInfo.firstAppItem = appItem;
+ lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+ }
+ mAdapterItems.add(appItem);
+ mFilteredApps.add(info);
+ }
+
+ // Merge multiple sections together as requested by the merge strategy for this device
+ mergeSections();
+
+ if (mNumAppsPerRow != 0) {
+ // Update the number of rows in the adapter after we do all the merging (otherwise, we
+ // would have to shift the values again)
+ int numAppsInSection = 0;
+ int numAppsInRow = 0;
+ int rowIndex = -1;
+ for (AdapterItem item : mAdapterItems) {
+ item.rowIndex = 0;
+ if (item.viewType == AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE) {
+ numAppsInSection = 0;
+ } else if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
+ item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ if (numAppsInSection % mNumAppsPerRow == 0) {
+ numAppsInRow = 0;
+ rowIndex++;
+ }
+ item.rowIndex = rowIndex;
+ item.rowAppIndex = numAppsInRow;
+ numAppsInSection++;
+ numAppsInRow++;
+ }
+ }
+ mNumAppRowsInAdapter = rowIndex + 1;
+
+ // Pre-calculate all the fast scroller fractions based on the number of rows
+ float rowFraction = 1f / mNumAppRowsInAdapter;
+ for (FastScrollSectionInfo info : mFastScrollerSections) {
+ AdapterItem item = info.fastScrollToItem;
+ if (item.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE &&
+ item.viewType != AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+ info.touchFraction = 0f;
+ continue;
+ }
+
+ float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow);
+ info.touchFraction = item.rowIndex * rowFraction + subRowFraction;
+ }
+ }
+
+ // Refresh the recycler view
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ private List<AppInfo> getFiltersAppInfos() {
+ if (mSearchResults == null) {
+ return mApps;
+ }
+
+ ArrayList<AppInfo> result = new ArrayList<>();
+ for (ComponentKey key : mSearchResults) {
+ AppInfo match = mComponentToAppMap.get(key);
+ if (match != null) {
+ result.add(match);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Merges multiple sections to reduce visual raggedness.
+ */
+ private void mergeSections() {
+ // Ignore merging until we have an algorithm and a valid row size
+ if (mMergeAlgorithm == null || mNumAppsPerRow == 0) {
+ return;
+ }
+
+ // Go through each section and try and merge some of the sections
+ if (!hasFilter()) {
+ int sectionAppCount = 0;
+ for (int i = 0; i < mSections.size() - 1; i++) {
+ SectionInfo section = mSections.get(i);
+ sectionAppCount = section.numApps;
+ int mergeCount = 1;
+
+ // Merge rows based on the current strategy
+ while (i < (mSections.size() - 1) &&
+ mMergeAlgorithm.continueMerging(section, mSections.get(i + 1),
+ sectionAppCount, mNumAppsPerRow, mergeCount)) {
+ SectionInfo nextSection = mSections.remove(i + 1);
+
+ // Remove the next section break
+ mAdapterItems.remove(nextSection.sectionBreakItem);
+ int pos = mAdapterItems.indexOf(section.firstAppItem);
+
+ // Point the section for these new apps to the merged section
+ int nextPos = pos + section.numApps;
+ for (int j = nextPos; j < (nextPos + nextSection.numApps); j++) {
+ AdapterItem item = mAdapterItems.get(j);
+ item.sectionInfo = section;
+ item.sectionAppIndex += section.numApps;
+ }
+
+ // Update the following adapter items of the removed section item
+ pos = mAdapterItems.indexOf(nextSection.firstAppItem);
+ for (int j = pos; j < mAdapterItems.size(); j++) {
+ AdapterItem item = mAdapterItems.get(j);
+ item.position--;
+ }
+ section.numApps += nextSection.numApps;
+ sectionAppCount += nextSection.numApps;
+
+ if (DEBUG) {
+ Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName +
+ " to " + section.firstAppItem.sectionName +
+ " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow));
+ }
+ mergeCount++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the cached section name for the given title, recomputing and updating the cache if
+ * the title has no cached section name.
+ */
+ private String getAndUpdateCachedSectionName(CharSequence title) {
+ String sectionName = mCachedSectionNames.get(title);
+ if (sectionName == null) {
+ sectionName = mIndexer.computeSectionName(title);
+ mCachedSectionNames.put(title, sectionName);
+ }
+ return sectionName;
+ }
+}