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 20 import androidx.annotation.Nullable; 21 import androidx.recyclerview.widget.DiffUtil; 22 23 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; 24 import com.android.launcher3.model.data.AppInfo; 25 import com.android.launcher3.model.data.ItemInfo; 26 import com.android.launcher3.util.LabelComparator; 27 import com.android.launcher3.views.ActivityContext; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.Locale; 32 import java.util.Objects; 33 import java.util.TreeMap; 34 import java.util.function.Predicate; 35 import java.util.stream.Collectors; 36 import java.util.stream.Stream; 37 38 /** 39 * The alphabetically sorted list of applications. 40 * 41 * @param <T> Type of context inflating this view. 42 */ 43 public class AlphabeticalAppsList<T extends Context & ActivityContext> implements 44 AllAppsStore.OnUpdateListener { 45 46 public static final String TAG = "AlphabeticalAppsList"; 47 48 private final WorkProfileManager mWorkProviderManager; 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 final String sectionName; 57 // The item position 58 public final int position; 59 FastScrollSectionInfo(String sectionName, int position)60 public FastScrollSectionInfo(String sectionName, int position) { 61 this.sectionName = sectionName; 62 this.position = position; 63 } 64 } 65 66 67 private final T mActivityContext; 68 69 // The set of apps from the system 70 private final List<AppInfo> mApps = new ArrayList<>(); 71 @Nullable 72 private final AllAppsStore mAllAppsStore; 73 74 // The number of results in current adapter 75 private int mAccessibilityResultsCount = 0; 76 // The current set of adapter items 77 private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>(); 78 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 79 private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 80 81 // The of ordered component names as a result of a search query 82 private final ArrayList<AdapterItem> mSearchResults = new ArrayList<>(); 83 private BaseAllAppsAdapter<T> mAdapter; 84 private AppInfoComparator mAppNameComparator; 85 private final int mNumAppsPerRowAllApps; 86 private int mNumAppRowsInAdapter; 87 private Predicate<ItemInfo> mItemFilter; 88 AlphabeticalAppsList(Context context, @Nullable AllAppsStore appsStore, WorkProfileManager workProfileManager)89 public AlphabeticalAppsList(Context context, @Nullable AllAppsStore appsStore, 90 WorkProfileManager workProfileManager) { 91 mAllAppsStore = appsStore; 92 mActivityContext = ActivityContext.lookupContext(context); 93 mAppNameComparator = new AppInfoComparator(context); 94 mWorkProviderManager = workProfileManager; 95 mNumAppsPerRowAllApps = mActivityContext.getDeviceProfile().inv.numAllAppsColumns; 96 if (mAllAppsStore != null) { 97 mAllAppsStore.addUpdateListener(this); 98 } 99 } 100 updateItemFilter(Predicate<ItemInfo> itemFilter)101 public void updateItemFilter(Predicate<ItemInfo> itemFilter) { 102 this.mItemFilter = itemFilter; 103 onAppsUpdated(); 104 } 105 106 /** 107 * Sets the adapter to notify when this dataset changes. 108 */ setAdapter(BaseAllAppsAdapter<T> adapter)109 public void setAdapter(BaseAllAppsAdapter<T> adapter) { 110 mAdapter = adapter; 111 } 112 113 /** 114 * Returns fast scroller sections of all the current filtered applications. 115 */ getFastScrollerSections()116 public List<FastScrollSectionInfo> getFastScrollerSections() { 117 return mFastScrollerSections; 118 } 119 120 /** 121 * Returns the current filtered list of applications broken down into their sections. 122 */ getAdapterItems()123 public List<AdapterItem> getAdapterItems() { 124 return mAdapterItems; 125 } 126 127 /** 128 * Returns the child adapter item with IME launch focus. 129 */ getFocusedChild()130 public AdapterItem getFocusedChild() { 131 if (mAdapterItems.size() == 0 || getFocusedChildIndex() == -1) { 132 return null; 133 } 134 return mAdapterItems.get(getFocusedChildIndex()); 135 } 136 137 /** 138 * Returns the index of the child with IME launch focus. 139 */ getFocusedChildIndex()140 public int getFocusedChildIndex() { 141 for (AdapterItem item : mAdapterItems) { 142 if (item.isCountedForAccessibility()) { 143 return mAdapterItems.indexOf(item); 144 } 145 } 146 return -1; 147 } 148 149 /** 150 * Returns the number of rows of applications 151 */ getNumAppRows()152 public int getNumAppRows() { 153 return mNumAppRowsInAdapter; 154 } 155 156 /** 157 * Returns the number of applications in this list. 158 */ getNumFilteredApps()159 public int getNumFilteredApps() { 160 return mAccessibilityResultsCount; 161 } 162 163 /** 164 * Returns whether there are search results which will hide the A-Z list. 165 */ hasSearchResults()166 public boolean hasSearchResults() { 167 return !mSearchResults.isEmpty(); 168 } 169 170 /** 171 * Sets results list for search 172 */ setSearchResults(ArrayList<AdapterItem> results)173 public boolean setSearchResults(ArrayList<AdapterItem> results) { 174 if (Objects.equals(results, mSearchResults)) { 175 return false; 176 } 177 mSearchResults.clear(); 178 if (results != null) { 179 mSearchResults.addAll(results); 180 } 181 updateAdapterItems(); 182 return true; 183 } 184 185 /** 186 * Updates internals when the set of apps are updated. 187 */ 188 @Override onAppsUpdated()189 public void onAppsUpdated() { 190 if (mAllAppsStore == null) { 191 return; 192 } 193 // Sort the list of apps 194 mApps.clear(); 195 196 Stream<AppInfo> appSteam = Stream.of(mAllAppsStore.getApps()); 197 if (!hasSearchResults() && mItemFilter != null) { 198 appSteam = appSteam.filter(mItemFilter); 199 } 200 appSteam = appSteam.sorted(mAppNameComparator); 201 202 // As a special case for some languages (currently only Simplified Chinese), we may need to 203 // coalesce sections 204 Locale curLocale = mActivityContext.getResources().getConfiguration().locale; 205 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 206 if (localeRequiresSectionSorting) { 207 // Compute the section headers. We use a TreeMap with the section name comparator to 208 // ensure that the sections are ordered when we iterate over it later 209 appSteam = appSteam.collect(Collectors.groupingBy( 210 info -> info.sectionName, 211 () -> new TreeMap<>(new LabelComparator()), 212 Collectors.toCollection(ArrayList::new))) 213 .values() 214 .stream() 215 .flatMap(ArrayList::stream); 216 } 217 218 appSteam.forEachOrdered(mApps::add); 219 // Recompose the set of adapter items from the current set of apps 220 if (mSearchResults.isEmpty()) { 221 updateAdapterItems(); 222 } 223 } 224 225 /** 226 * Updates the set of filtered apps with the current filter. At this point, we expect 227 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 228 */ updateAdapterItems()229 public void updateAdapterItems() { 230 List<AdapterItem> oldItems = new ArrayList<>(mAdapterItems); 231 // Prepare to update the list of sections, filtered apps, etc. 232 mFastScrollerSections.clear(); 233 mAdapterItems.clear(); 234 mAccessibilityResultsCount = 0; 235 236 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 237 // ordered set of sections 238 if (hasSearchResults()) { 239 mAdapterItems.addAll(mSearchResults); 240 } else { 241 int position = 0; 242 boolean addApps = true; 243 if (mWorkProviderManager != null) { 244 position += mWorkProviderManager.addWorkItems(mAdapterItems); 245 addApps = mWorkProviderManager.shouldShowWorkApps(); 246 } 247 if (addApps) { 248 String lastSectionName = null; 249 for (AppInfo info : mApps) { 250 mAdapterItems.add(AdapterItem.asApp(info)); 251 252 String sectionName = info.sectionName; 253 // Create a new section if the section names do not match 254 if (!sectionName.equals(lastSectionName)) { 255 lastSectionName = sectionName; 256 mFastScrollerSections.add(new FastScrollSectionInfo(sectionName, position)); 257 } 258 position++; 259 } 260 } 261 } 262 mAccessibilityResultsCount = (int) mAdapterItems.stream() 263 .filter(AdapterItem::isCountedForAccessibility).count(); 264 265 if (mNumAppsPerRowAllApps != 0) { 266 // Update the number of rows in the adapter after we do all the merging (otherwise, we 267 // would have to shift the values again) 268 int numAppsInSection = 0; 269 int numAppsInRow = 0; 270 int rowIndex = -1; 271 for (AdapterItem item : mAdapterItems) { 272 item.rowIndex = 0; 273 if (BaseAllAppsAdapter.isDividerViewType(item.viewType)) { 274 numAppsInSection = 0; 275 } else if (BaseAllAppsAdapter.isIconViewType(item.viewType)) { 276 if (numAppsInSection % mNumAppsPerRowAllApps == 0) { 277 numAppsInRow = 0; 278 rowIndex++; 279 } 280 item.rowIndex = rowIndex; 281 item.rowAppIndex = numAppsInRow; 282 numAppsInSection++; 283 numAppsInRow++; 284 } 285 } 286 mNumAppRowsInAdapter = rowIndex + 1; 287 } 288 289 if (mAdapter != null) { 290 DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false) 291 .dispatchUpdatesTo(mAdapter); 292 } 293 } 294 295 private static class MyDiffCallback extends DiffUtil.Callback { 296 297 private final List<AdapterItem> mOldList; 298 private final List<AdapterItem> mNewList; 299 MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList)300 MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList) { 301 mOldList = oldList; 302 mNewList = newList; 303 } 304 305 @Override getOldListSize()306 public int getOldListSize() { 307 return mOldList.size(); 308 } 309 310 @Override getNewListSize()311 public int getNewListSize() { 312 return mNewList.size(); 313 } 314 315 @Override areItemsTheSame(int oldItemPosition, int newItemPosition)316 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 317 return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition)); 318 } 319 320 @Override areContentsTheSame(int oldItemPosition, int newItemPosition)321 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 322 return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition)); 323 } 324 } 325 326 } 327