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 static com.android.launcher3.touch.ItemLongClickListener.INSTANCE_ALL_APPS; 19 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.Resources; 23 import android.view.Gravity; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.View.OnClickListener; 27 import android.view.View.OnFocusChangeListener; 28 import android.view.View.OnLongClickListener; 29 import android.view.ViewGroup; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.widget.TextView; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.core.view.accessibility.AccessibilityEventCompat; 36 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 37 import androidx.core.view.accessibility.AccessibilityRecordCompat; 38 import androidx.recyclerview.widget.GridLayoutManager; 39 import androidx.recyclerview.widget.RecyclerView; 40 41 import com.android.launcher3.BaseDraggingActivity; 42 import com.android.launcher3.BubbleTextView; 43 import com.android.launcher3.R; 44 import com.android.launcher3.config.FeatureFlags; 45 import com.android.launcher3.model.data.AppInfo; 46 import com.android.launcher3.model.data.ItemInfoWithIcon; 47 import com.android.launcher3.util.PackageManagerHelper; 48 49 import java.util.Arrays; 50 import java.util.List; 51 52 /** 53 * The grid view adapter of all the apps. 54 */ 55 public class AllAppsGridAdapter extends 56 RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 57 58 public static final String TAG = "AppsGridAdapter"; 59 60 // A normal icon 61 public static final int VIEW_TYPE_ICON = 1 << 1; 62 // The message shown when there are no filtered results 63 public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2; 64 // The message to continue to a market search when there are no filtered results 65 public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3; 66 67 // We use various dividers for various purposes. They share enough attributes to reuse layouts, 68 // but differ in enough attributes to require different view types 69 70 // A divider that separates the apps list and the search market button 71 public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4; 72 73 // Common view type masks 74 public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; 75 public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; 76 77 78 private final BaseAdapterProvider[] mAdapterProviders; 79 80 /** 81 * ViewHolder for each icon. 82 */ 83 public static class ViewHolder extends RecyclerView.ViewHolder { 84 ViewHolder(View v)85 public ViewHolder(View v) { 86 super(v); 87 } 88 } 89 90 /** 91 * Info about a particular adapter item (can be either section or app) 92 */ 93 public static class AdapterItem { 94 /** Common properties */ 95 // The index of this adapter item in the list 96 public int position; 97 // The type of this item 98 public int viewType; 99 100 // The section name of this item. Note that there can be multiple items with different 101 // sectionNames in the same section 102 public String sectionName = null; 103 // The row that this item shows up on 104 public int rowIndex; 105 // The index of this app in the row 106 public int rowAppIndex; 107 // The associated ItemInfoWithIcon for the item 108 public ItemInfoWithIcon itemInfo = null; 109 // The index of this app not including sections 110 public int appIndex = -1; 111 // Search section associated to result 112 public DecorationInfo decorationInfo = null; 113 114 /** 115 * Factory method for AppIcon AdapterItem 116 */ asApp(int pos, String sectionName, AppInfo appInfo, int appIndex)117 public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, 118 int appIndex) { 119 AdapterItem item = new AdapterItem(); 120 item.viewType = VIEW_TYPE_ICON; 121 item.position = pos; 122 item.sectionName = sectionName; 123 item.itemInfo = appInfo; 124 item.appIndex = appIndex; 125 return item; 126 } 127 128 /** 129 * Factory method for empty search results view 130 */ asEmptySearch(int pos)131 public static AdapterItem asEmptySearch(int pos) { 132 AdapterItem item = new AdapterItem(); 133 item.viewType = VIEW_TYPE_EMPTY_SEARCH; 134 item.position = pos; 135 return item; 136 } 137 138 /** 139 * Factory method for a dividerView in AllAppsSearch 140 */ asAllAppsDivider(int pos)141 public static AdapterItem asAllAppsDivider(int pos) { 142 AdapterItem item = new AdapterItem(); 143 item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER; 144 item.position = pos; 145 return item; 146 } 147 148 /** 149 * Factory method for a market search button 150 */ asMarketSearch(int pos)151 public static AdapterItem asMarketSearch(int pos) { 152 AdapterItem item = new AdapterItem(); 153 item.viewType = VIEW_TYPE_SEARCH_MARKET; 154 item.position = pos; 155 return item; 156 } 157 isCountedForAccessibility()158 protected boolean isCountedForAccessibility() { 159 return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET; 160 } 161 } 162 163 /** 164 * A subclass of GridLayoutManager that overrides accessibility values during app search. 165 */ 166 public class AppsGridLayoutManager extends GridLayoutManager { 167 AppsGridLayoutManager(Context context)168 public AppsGridLayoutManager(Context context) { 169 super(context, 1, GridLayoutManager.VERTICAL, false); 170 } 171 172 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)173 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 174 super.onInitializeAccessibilityEvent(event); 175 176 // Ensure that we only report the number apps for accessibility not including other 177 // adapter views 178 final AccessibilityRecordCompat record = AccessibilityEventCompat 179 .asRecord(event); 180 record.setItemCount(mApps.getNumFilteredApps()); 181 record.setFromIndex(Math.max(0, 182 record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); 183 record.setToIndex(Math.max(0, 184 record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); 185 } 186 187 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)188 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 189 RecyclerView.State state) { 190 return super.getRowCountForAccessibility(recycler, state) - 191 getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); 192 } 193 194 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)195 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 196 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 197 super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); 198 199 ViewGroup.LayoutParams lp = host.getLayoutParams(); 200 AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); 201 if (!(lp instanceof LayoutParams) || (cic == null)) { 202 return; 203 } 204 LayoutParams glp = (LayoutParams) lp; 205 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 206 cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), 207 cic.getRowSpan(), 208 cic.getColumnIndex(), 209 cic.getColumnSpan(), 210 cic.isHeading(), 211 cic.isSelected())); 212 } 213 214 /** 215 * Returns the number of rows before {@param adapterPosition}, including this position 216 * which should not be counted towards the collection info. 217 */ getRowsNotForAccessibility(int adapterPosition)218 private int getRowsNotForAccessibility(int adapterPosition) { 219 List<AdapterItem> items = mApps.getAdapterItems(); 220 adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); 221 int extraRows = 0; 222 for (int i = 0; i <= adapterPosition; i++) { 223 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_ICON)) { 224 extraRows++; 225 } 226 } 227 return extraRows; 228 } 229 } 230 231 /** 232 * Helper class to size the grid items. 233 */ 234 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 235 GridSpanSizer()236 public GridSpanSizer() { 237 super(); 238 setSpanIndexCacheEnabled(true); 239 } 240 241 @Override getSpanSize(int position)242 public int getSpanSize(int position) { 243 int viewType = mApps.getAdapterItems().get(position).viewType; 244 int totalSpans = mGridLayoutMgr.getSpanCount(); 245 if (isIconViewType(viewType)) { 246 return totalSpans / mAppsPerRow; 247 } else { 248 BaseAdapterProvider adapterProvider = getAdapterProvider(viewType); 249 if (adapterProvider != null) { 250 return totalSpans / adapterProvider.getItemsPerRow(viewType, mAppsPerRow); 251 } 252 253 // Section breaks span the full width 254 return totalSpans; 255 } 256 } 257 } 258 259 private final BaseDraggingActivity mLauncher; 260 private final LayoutInflater mLayoutInflater; 261 private final AlphabeticalAppsList mApps; 262 private final GridLayoutManager mGridLayoutMgr; 263 private final GridSpanSizer mGridSizer; 264 265 private final OnClickListener mOnIconClickListener; 266 private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS; 267 268 private int mAppsPerRow; 269 270 private OnFocusChangeListener mIconFocusListener; 271 272 // The text to show when there are no search results and no market search handler. 273 protected String mEmptySearchMessage; 274 // The intent to send off to the market app, updated each time the search query changes. 275 private Intent mMarketSearchIntent; 276 277 private final int mExtraHeight; 278 AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders)279 public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, 280 AlphabeticalAppsList apps, BaseAdapterProvider[] adapterProviders) { 281 Resources res = launcher.getResources(); 282 mLauncher = launcher; 283 mApps = apps; 284 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 285 mGridSizer = new GridSpanSizer(); 286 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 287 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 288 mLayoutInflater = inflater; 289 290 mOnIconClickListener = launcher.getItemOnClickListener(); 291 292 mAdapterProviders = adapterProviders; 293 setAppsPerRow(mLauncher.getDeviceProfile().numShownAllAppsColumns); 294 mExtraHeight = launcher.getResources().getDimensionPixelSize(R.dimen.all_apps_height_extra); 295 } 296 setAppsPerRow(int appsPerRow)297 public void setAppsPerRow(int appsPerRow) { 298 mAppsPerRow = appsPerRow; 299 int totalSpans = mAppsPerRow; 300 for (BaseAdapterProvider adapterProvider : mAdapterProviders) { 301 for (int itemPerRow : adapterProvider.getSupportedItemsPerRowArray()) { 302 if (totalSpans % itemPerRow != 0) { 303 totalSpans *= itemPerRow; 304 } 305 } 306 } 307 mGridLayoutMgr.setSpanCount(totalSpans); 308 } 309 310 /** 311 * Sets the long click listener for icons 312 */ setOnIconLongClickListener(@ullable OnLongClickListener listener)313 public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) { 314 mOnIconLongClickListener = listener; 315 } 316 isDividerViewType(int viewType)317 public static boolean isDividerViewType(int viewType) { 318 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 319 } 320 isIconViewType(int viewType)321 public static boolean isIconViewType(int viewType) { 322 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 323 } 324 isViewType(int viewType, int viewTypeMask)325 public static boolean isViewType(int viewType, int viewTypeMask) { 326 return (viewType & viewTypeMask) != 0; 327 } 328 setIconFocusListener(OnFocusChangeListener focusListener)329 public void setIconFocusListener(OnFocusChangeListener focusListener) { 330 mIconFocusListener = focusListener; 331 } 332 333 /** 334 * Sets the last search query that was made, used to show when there are no results and to also 335 * seed the intent for searching the market. 336 */ setLastSearchQuery(String query)337 public void setLastSearchQuery(String query) { 338 Resources res = mLauncher.getResources(); 339 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 340 mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); 341 } 342 343 /** 344 * Returns the grid layout manager. 345 */ getLayoutManager()346 public GridLayoutManager getLayoutManager() { 347 return mGridLayoutMgr; 348 } 349 350 @Override onCreateViewHolder(ViewGroup parent, int viewType)351 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 352 switch (viewType) { 353 case VIEW_TYPE_ICON: 354 int layout = !FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() ? R.layout.all_apps_icon 355 : R.layout.all_apps_icon_twoline; 356 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 357 layout, parent, false); 358 icon.setLongPressTimeoutFactor(1f); 359 icon.setOnFocusChangeListener(mIconFocusListener); 360 icon.setOnClickListener(mOnIconClickListener); 361 icon.setOnLongClickListener(mOnIconLongClickListener); 362 // Ensure the all apps icon height matches the workspace icons in portrait mode. 363 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; 364 if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) { 365 icon.getLayoutParams().height += mExtraHeight; 366 } 367 return new ViewHolder(icon); 368 case VIEW_TYPE_EMPTY_SEARCH: 369 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 370 parent, false)); 371 case VIEW_TYPE_SEARCH_MARKET: 372 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 373 parent, false); 374 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely( 375 v, mMarketSearchIntent, null)); 376 return new ViewHolder(searchMarketView); 377 case VIEW_TYPE_ALL_APPS_DIVIDER: 378 return new ViewHolder(mLayoutInflater.inflate( 379 R.layout.all_apps_divider, parent, false)); 380 default: 381 BaseAdapterProvider adapterProvider = getAdapterProvider(viewType); 382 if (adapterProvider != null) { 383 return adapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType); 384 } 385 throw new RuntimeException("Unexpected view type" + viewType); 386 } 387 } 388 389 @Override onBindViewHolder(ViewHolder holder, int position)390 public void onBindViewHolder(ViewHolder holder, int position) { 391 switch (holder.getItemViewType()) { 392 case VIEW_TYPE_ICON: 393 AdapterItem adapterItem = mApps.getAdapterItems().get(position); 394 BubbleTextView icon = (BubbleTextView) holder.itemView; 395 icon.reset(); 396 if (adapterItem.itemInfo instanceof AppInfo) { 397 icon.applyFromApplicationInfo((AppInfo) adapterItem.itemInfo); 398 } else { 399 icon.applyFromItemInfoWithIcon(adapterItem.itemInfo); 400 } 401 break; 402 case VIEW_TYPE_EMPTY_SEARCH: 403 TextView emptyViewText = (TextView) holder.itemView; 404 emptyViewText.setText(mEmptySearchMessage); 405 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 406 Gravity.START | Gravity.CENTER_VERTICAL); 407 break; 408 case VIEW_TYPE_SEARCH_MARKET: 409 TextView searchView = (TextView) holder.itemView; 410 if (mMarketSearchIntent != null) { 411 searchView.setVisibility(View.VISIBLE); 412 } else { 413 searchView.setVisibility(View.GONE); 414 } 415 break; 416 case VIEW_TYPE_ALL_APPS_DIVIDER: 417 // nothing to do 418 break; 419 default: 420 BaseAdapterProvider adapterProvider = getAdapterProvider(holder.getItemViewType()); 421 if (adapterProvider != null) { 422 adapterProvider.onBindView(holder, position); 423 } 424 } 425 } 426 427 @Override onViewRecycled(@onNull ViewHolder holder)428 public void onViewRecycled(@NonNull ViewHolder holder) { 429 super.onViewRecycled(holder); 430 } 431 432 @Override onFailedToRecycleView(ViewHolder holder)433 public boolean onFailedToRecycleView(ViewHolder holder) { 434 // Always recycle and we will reset the view when it is bound 435 return true; 436 } 437 438 @Override getItemCount()439 public int getItemCount() { 440 return mApps.getAdapterItems().size(); 441 } 442 443 @Override getItemViewType(int position)444 public int getItemViewType(int position) { 445 AdapterItem item = mApps.getAdapterItems().get(position); 446 return item.viewType; 447 } 448 449 @Nullable getAdapterProvider(int viewType)450 private BaseAdapterProvider getAdapterProvider(int viewType) { 451 return Arrays.stream(mAdapterProviders).filter( 452 adapterProvider -> adapterProvider.isViewSupported(viewType)).findFirst().orElse( 453 null); 454 } 455 } 456