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.content.pm.PackageManager; 20 21 import com.android.launcher3.AppInfo; 22 import com.android.launcher3.Launcher; 23 import com.android.launcher3.Utilities; 24 import com.android.launcher3.compat.AlphabeticIndexCompat; 25 import com.android.launcher3.shortcuts.DeepShortcutManager; 26 import com.android.launcher3.util.ComponentKey; 27 import com.android.launcher3.util.ItemInfoMatcher; 28 import com.android.launcher3.util.LabelComparator; 29 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.List; 34 import java.util.Locale; 35 import java.util.Map; 36 import java.util.TreeMap; 37 38 /** 39 * The alphabetically sorted list of applications. 40 */ 41 public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { 42 43 public static final String TAG = "AlphabeticalAppsList"; 44 45 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION = 0; 46 private static final int FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS = 1; 47 48 private final int mFastScrollDistributionMode = FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS; 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 String sectionName; 57 // The AdapterItem to scroll to for this section 58 public AdapterItem fastScrollToItem; 59 // The touch fraction that should map to this fast scroll section info 60 public float touchFraction; 61 62 public FastScrollSectionInfo(String sectionName) { 63 this.sectionName = sectionName; 64 } 65 } 66 67 /** 68 * Info about a particular adapter item (can be either section or app) 69 */ 70 public static class AdapterItem { 71 /** Common properties */ 72 // The index of this adapter item in the list 73 public int position; 74 // The type of this item 75 public int viewType; 76 77 /** App-only properties */ 78 // The section name of this app. Note that there can be multiple items with different 79 // sectionNames in the same section 80 public String sectionName = null; 81 // The row that this item shows up on 82 public int rowIndex; 83 // The index of this app in the row 84 public int rowAppIndex; 85 // The associated AppInfo for the app 86 public AppInfo appInfo = null; 87 // The index of this app not including sections 88 public int appIndex = -1; 89 90 public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, 91 int appIndex) { 92 AdapterItem item = new AdapterItem(); 93 item.viewType = AllAppsGridAdapter.VIEW_TYPE_ICON; 94 item.position = pos; 95 item.sectionName = sectionName; 96 item.appInfo = appInfo; 97 item.appIndex = appIndex; 98 return item; 99 } 100 101 public static AdapterItem asEmptySearch(int pos) { 102 AdapterItem item = new AdapterItem(); 103 item.viewType = AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH; 104 item.position = pos; 105 return item; 106 } 107 108 public static AdapterItem asAllAppsDivider(int pos) { 109 AdapterItem item = new AdapterItem(); 110 item.viewType = AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER; 111 item.position = pos; 112 return item; 113 } 114 115 public static AdapterItem asMarketSearch(int pos) { 116 AdapterItem item = new AdapterItem(); 117 item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET; 118 item.position = pos; 119 return item; 120 } 121 122 public static AdapterItem asWorkTabFooter(int pos) { 123 AdapterItem item = new AdapterItem(); 124 item.viewType = AllAppsGridAdapter.VIEW_TYPE_WORK_TAB_FOOTER; 125 item.position = pos; 126 return item; 127 } 128 } 129 130 private final Launcher mLauncher; 131 132 // The set of apps from the system 133 private final List<AppInfo> mApps = new ArrayList<>(); 134 private final AllAppsStore mAllAppsStore; 135 136 // The set of filtered apps with the current filter 137 private final List<AppInfo> mFilteredApps = new ArrayList<>(); 138 // The current set of adapter items 139 private final ArrayList<AdapterItem> mAdapterItems = new ArrayList<>(); 140 // The set of sections that we allow fast-scrolling to (includes non-merged sections) 141 private final List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>(); 142 // Is it the work profile app list. 143 private final boolean mIsWork; 144 145 // The of ordered component names as a result of a search query 146 private ArrayList<ComponentKey> mSearchResults; 147 private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>(); 148 private AllAppsGridAdapter mAdapter; 149 private AlphabeticIndexCompat mIndexer; 150 private AppInfoComparator mAppNameComparator; 151 private final int mNumAppsPerRow; 152 private int mNumAppRowsInAdapter; 153 private ItemInfoMatcher mItemFilter; 154 155 public AlphabeticalAppsList(Context context, AllAppsStore appsStore, boolean isWork) { 156 mAllAppsStore = appsStore; 157 mLauncher = Launcher.getLauncher(context); 158 mIndexer = new AlphabeticIndexCompat(context); 159 mAppNameComparator = new AppInfoComparator(context); 160 mIsWork = isWork; 161 mNumAppsPerRow = mLauncher.getDeviceProfile().inv.numColumns; 162 mAllAppsStore.addUpdateListener(this); 163 } 164 165 public void updateItemFilter(ItemInfoMatcher itemFilter) { 166 this.mItemFilter = itemFilter; 167 onAppsUpdated(); 168 } 169 170 /** 171 * Sets the adapter to notify when this dataset changes. 172 */ 173 public void setAdapter(AllAppsGridAdapter adapter) { 174 mAdapter = adapter; 175 } 176 177 /** 178 * Returns all the apps. 179 */ 180 public List<AppInfo> getApps() { 181 return mApps; 182 } 183 184 /** 185 * Returns fast scroller sections of all the current filtered applications. 186 */ 187 public List<FastScrollSectionInfo> getFastScrollerSections() { 188 return mFastScrollerSections; 189 } 190 191 /** 192 * Returns the current filtered list of applications broken down into their sections. 193 */ 194 public List<AdapterItem> getAdapterItems() { 195 return mAdapterItems; 196 } 197 198 /** 199 * Returns the number of rows of applications 200 */ 201 public int getNumAppRows() { 202 return mNumAppRowsInAdapter; 203 } 204 205 /** 206 * Returns the number of applications in this list. 207 */ 208 public int getNumFilteredApps() { 209 return mFilteredApps.size(); 210 } 211 212 /** 213 * Returns whether there are is a filter set. 214 */ 215 public boolean hasFilter() { 216 return (mSearchResults != null); 217 } 218 219 /** 220 * Returns whether there are no filtered results. 221 */ 222 public boolean hasNoFilteredResults() { 223 return (mSearchResults != null) && mFilteredApps.isEmpty(); 224 } 225 226 /** 227 * Sets the sorted list of filtered components. 228 */ 229 public boolean setOrderedFilter(ArrayList<ComponentKey> f) { 230 if (mSearchResults != f) { 231 boolean same = mSearchResults != null && mSearchResults.equals(f); 232 mSearchResults = f; 233 onAppsUpdated(); 234 return !same; 235 } 236 return false; 237 } 238 239 /** 240 * Updates internals when the set of apps are updated. 241 */ 242 @Override 243 public void onAppsUpdated() { 244 // Sort the list of apps 245 mApps.clear(); 246 247 for (AppInfo app : mAllAppsStore.getApps()) { 248 if (mItemFilter == null || mItemFilter.matches(app, null) || hasFilter()) { 249 mApps.add(app); 250 } 251 } 252 253 Collections.sort(mApps, mAppNameComparator); 254 255 // As a special case for some languages (currently only Simplified Chinese), we may need to 256 // coalesce sections 257 Locale curLocale = mLauncher.getResources().getConfiguration().locale; 258 boolean localeRequiresSectionSorting = curLocale.equals(Locale.SIMPLIFIED_CHINESE); 259 if (localeRequiresSectionSorting) { 260 // Compute the section headers. We use a TreeMap with the section name comparator to 261 // ensure that the sections are ordered when we iterate over it later 262 TreeMap<String, ArrayList<AppInfo>> sectionMap = new TreeMap<>(new LabelComparator()); 263 for (AppInfo info : mApps) { 264 // Add the section to the cache 265 String sectionName = getAndUpdateCachedSectionName(info.title); 266 267 // Add it to the mapping 268 ArrayList<AppInfo> sectionApps = sectionMap.get(sectionName); 269 if (sectionApps == null) { 270 sectionApps = new ArrayList<>(); 271 sectionMap.put(sectionName, sectionApps); 272 } 273 sectionApps.add(info); 274 } 275 276 // Add each of the section apps to the list in order 277 mApps.clear(); 278 for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) { 279 mApps.addAll(entry.getValue()); 280 } 281 } else { 282 // Just compute the section headers for use below 283 for (AppInfo info : mApps) { 284 // Add the section to the cache 285 getAndUpdateCachedSectionName(info.title); 286 } 287 } 288 289 // Recompose the set of adapter items from the current set of apps 290 updateAdapterItems(); 291 } 292 293 /** 294 * Updates the set of filtered apps with the current filter. At this point, we expect 295 * mCachedSectionNames to have been calculated for the set of all apps in mApps. 296 */ 297 private void updateAdapterItems() { 298 refillAdapterItems(); 299 refreshRecyclerView(); 300 } 301 302 private void refreshRecyclerView() { 303 if (mAdapter != null) { 304 mAdapter.notifyDataSetChanged(); 305 } 306 } 307 308 private void refillAdapterItems() { 309 String lastSectionName = null; 310 FastScrollSectionInfo lastFastScrollerSectionInfo = null; 311 int position = 0; 312 int appIndex = 0; 313 314 // Prepare to update the list of sections, filtered apps, etc. 315 mFilteredApps.clear(); 316 mFastScrollerSections.clear(); 317 mAdapterItems.clear(); 318 319 // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the 320 // ordered set of sections 321 for (AppInfo info : getFiltersAppInfos()) { 322 String sectionName = getAndUpdateCachedSectionName(info.title); 323 324 // Create a new section if the section names do not match 325 if (!sectionName.equals(lastSectionName)) { 326 lastSectionName = sectionName; 327 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName); 328 mFastScrollerSections.add(lastFastScrollerSectionInfo); 329 } 330 331 // Create an app item 332 AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++); 333 if (lastFastScrollerSectionInfo.fastScrollToItem == null) { 334 lastFastScrollerSectionInfo.fastScrollToItem = appItem; 335 } 336 mAdapterItems.add(appItem); 337 mFilteredApps.add(info); 338 } 339 340 if (hasFilter()) { 341 // Append the search market item 342 if (hasNoFilteredResults()) { 343 mAdapterItems.add(AdapterItem.asEmptySearch(position++)); 344 } else { 345 mAdapterItems.add(AdapterItem.asAllAppsDivider(position++)); 346 } 347 mAdapterItems.add(AdapterItem.asMarketSearch(position++)); 348 } 349 350 if (mNumAppsPerRow != 0) { 351 // Update the number of rows in the adapter after we do all the merging (otherwise, we 352 // would have to shift the values again) 353 int numAppsInSection = 0; 354 int numAppsInRow = 0; 355 int rowIndex = -1; 356 for (AdapterItem item : mAdapterItems) { 357 item.rowIndex = 0; 358 if (AllAppsGridAdapter.isDividerViewType(item.viewType)) { 359 numAppsInSection = 0; 360 } else if (AllAppsGridAdapter.isIconViewType(item.viewType)) { 361 if (numAppsInSection % mNumAppsPerRow == 0) { 362 numAppsInRow = 0; 363 rowIndex++; 364 } 365 item.rowIndex = rowIndex; 366 item.rowAppIndex = numAppsInRow; 367 numAppsInSection++; 368 numAppsInRow++; 369 } 370 } 371 mNumAppRowsInAdapter = rowIndex + 1; 372 373 // Pre-calculate all the fast scroller fractions 374 switch (mFastScrollDistributionMode) { 375 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_ROWS_FRACTION: 376 float rowFraction = 1f / mNumAppRowsInAdapter; 377 for (FastScrollSectionInfo info : mFastScrollerSections) { 378 AdapterItem item = info.fastScrollToItem; 379 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 380 info.touchFraction = 0f; 381 continue; 382 } 383 384 float subRowFraction = item.rowAppIndex * (rowFraction / mNumAppsPerRow); 385 info.touchFraction = item.rowIndex * rowFraction + subRowFraction; 386 } 387 break; 388 case FAST_SCROLL_FRACTION_DISTRIBUTE_BY_NUM_SECTIONS: 389 float perSectionTouchFraction = 1f / mFastScrollerSections.size(); 390 float cumulativeTouchFraction = 0f; 391 for (FastScrollSectionInfo info : mFastScrollerSections) { 392 AdapterItem item = info.fastScrollToItem; 393 if (!AllAppsGridAdapter.isIconViewType(item.viewType)) { 394 info.touchFraction = 0f; 395 continue; 396 } 397 info.touchFraction = cumulativeTouchFraction; 398 cumulativeTouchFraction += perSectionTouchFraction; 399 } 400 break; 401 } 402 } 403 404 // Add the work profile footer if required. 405 if (shouldShowWorkFooter()) { 406 mAdapterItems.add(AdapterItem.asWorkTabFooter(position++)); 407 } 408 } 409 410 private boolean shouldShowWorkFooter() { 411 return mIsWork && Utilities.ATLEAST_P && 412 (DeepShortcutManager.getInstance(mLauncher).hasHostPermission() 413 || mLauncher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE") 414 == PackageManager.PERMISSION_GRANTED); 415 } 416 417 private List<AppInfo> getFiltersAppInfos() { 418 if (mSearchResults == null) { 419 return mApps; 420 } 421 ArrayList<AppInfo> result = new ArrayList<>(); 422 for (ComponentKey key : mSearchResults) { 423 AppInfo match = mAllAppsStore.getApp(key); 424 if (match != null) { 425 result.add(match); 426 } 427 } 428 return result; 429 } 430 431 /** 432 * Returns the cached section name for the given title, recomputing and updating the cache if 433 * the title has no cached section name. 434 */ 435 private String getAndUpdateCachedSectionName(CharSequence title) { 436 String sectionName = mCachedSectionNames.get(title); 437 if (sectionName == null) { 438 sectionName = mIndexer.computeSectionName(title); 439 mCachedSectionNames.put(title, sectionName); 440 } 441 return sectionName; 442 } 443 444 } 445