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