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