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