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