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