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.Nullable; 34 import androidx.core.view.accessibility.AccessibilityEventCompat; 35 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 36 import androidx.core.view.accessibility.AccessibilityRecordCompat; 37 import androidx.recyclerview.widget.GridLayoutManager; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.launcher3.BaseDraggingActivity; 41 import com.android.launcher3.BubbleTextView; 42 import com.android.launcher3.R; 43 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; 44 import com.android.launcher3.model.AppLaunchTracker; 45 import com.android.launcher3.model.data.AppInfo; 46 import com.android.launcher3.util.PackageManagerHelper; 47 48 import java.util.List; 49 50 /** 51 * The grid view adapter of all the apps. 52 */ 53 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 54 55 public static final String TAG = "AppsGridAdapter"; 56 57 // A normal icon 58 public static final int VIEW_TYPE_ICON = 1 << 1; 59 // The message shown when there are no filtered results 60 public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2; 61 // The message to continue to a market search when there are no filtered results 62 public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 3; 63 64 // We use various dividers for various purposes. They share enough attributes to reuse layouts, 65 // but differ in enough attributes to require different view types 66 67 // A divider that separates the apps list and the search market button 68 public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4; 69 70 // Common view type masks 71 public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; 72 public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON; 73 74 /** 75 * ViewHolder for each icon. 76 */ 77 public static class ViewHolder extends RecyclerView.ViewHolder { 78 ViewHolder(View v)79 public ViewHolder(View v) { 80 super(v); 81 } 82 } 83 84 /** 85 * A subclass of GridLayoutManager that overrides accessibility values during app search. 86 */ 87 public class AppsGridLayoutManager extends GridLayoutManager { 88 AppsGridLayoutManager(Context context)89 public AppsGridLayoutManager(Context context) { 90 super(context, 1, GridLayoutManager.VERTICAL, false); 91 } 92 93 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)94 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 95 super.onInitializeAccessibilityEvent(event); 96 97 // Ensure that we only report the number apps for accessibility not including other 98 // adapter views 99 final AccessibilityRecordCompat record = AccessibilityEventCompat 100 .asRecord(event); 101 record.setItemCount(mApps.getNumFilteredApps()); 102 record.setFromIndex(Math.max(0, 103 record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); 104 record.setToIndex(Math.max(0, 105 record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); 106 } 107 108 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)109 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 110 RecyclerView.State state) { 111 return super.getRowCountForAccessibility(recycler, state) - 112 getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); 113 } 114 115 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)116 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 117 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 118 super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); 119 120 ViewGroup.LayoutParams lp = host.getLayoutParams(); 121 AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); 122 if (!(lp instanceof LayoutParams) || (cic == null)) { 123 return; 124 } 125 LayoutParams glp = (LayoutParams) lp; 126 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 127 cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), 128 cic.getRowSpan(), 129 cic.getColumnIndex(), 130 cic.getColumnSpan(), 131 cic.isHeading(), 132 cic.isSelected())); 133 } 134 135 /** 136 * Returns the number of rows before {@param adapterPosition}, including this position 137 * which should not be counted towards the collection info. 138 */ getRowsNotForAccessibility(int adapterPosition)139 private int getRowsNotForAccessibility(int adapterPosition) { 140 List<AdapterItem> items = mApps.getAdapterItems(); 141 adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); 142 int extraRows = 0; 143 for (int i = 0; i <= adapterPosition; i++) { 144 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_ICON)) { 145 extraRows++; 146 } 147 } 148 return extraRows; 149 } 150 } 151 152 /** 153 * Helper class to size the grid items. 154 */ 155 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 156 GridSpanSizer()157 public GridSpanSizer() { 158 super(); 159 setSpanIndexCacheEnabled(true); 160 } 161 162 @Override getSpanSize(int position)163 public int getSpanSize(int position) { 164 if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { 165 return 1; 166 } else { 167 // Section breaks span the full width 168 return mAppsPerRow; 169 } 170 } 171 } 172 173 private final BaseDraggingActivity mLauncher; 174 private final LayoutInflater mLayoutInflater; 175 private final AlphabeticalAppsList mApps; 176 private final GridLayoutManager mGridLayoutMgr; 177 private final GridSpanSizer mGridSizer; 178 179 private final OnClickListener mOnIconClickListener; 180 private OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS; 181 182 private int mAppsPerRow; 183 184 private OnFocusChangeListener mIconFocusListener; 185 186 // The text to show when there are no search results and no market search handler. 187 protected String mEmptySearchMessage; 188 // The intent to send off to the market app, updated each time the search query changes. 189 private Intent mMarketSearchIntent; 190 AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, AlphabeticalAppsList apps)191 public AllAppsGridAdapter(BaseDraggingActivity launcher, LayoutInflater inflater, 192 AlphabeticalAppsList apps) { 193 Resources res = launcher.getResources(); 194 mLauncher = launcher; 195 mApps = apps; 196 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 197 mGridSizer = new GridSpanSizer(); 198 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 199 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 200 mLayoutInflater = inflater; 201 202 mOnIconClickListener = launcher.getItemOnClickListener(); 203 204 setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns); 205 } 206 setAppsPerRow(int appsPerRow)207 public void setAppsPerRow(int appsPerRow) { 208 mAppsPerRow = appsPerRow; 209 mGridLayoutMgr.setSpanCount(mAppsPerRow); 210 } 211 212 /** 213 * Sets the long click listener for icons 214 */ setOnIconLongClickListener(@ullable OnLongClickListener listener)215 public void setOnIconLongClickListener(@Nullable OnLongClickListener listener) { 216 mOnIconLongClickListener = listener; 217 } 218 isDividerViewType(int viewType)219 public static boolean isDividerViewType(int viewType) { 220 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 221 } 222 isIconViewType(int viewType)223 public static boolean isIconViewType(int viewType) { 224 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 225 } 226 isViewType(int viewType, int viewTypeMask)227 public static boolean isViewType(int viewType, int viewTypeMask) { 228 return (viewType & viewTypeMask) != 0; 229 } 230 setIconFocusListener(OnFocusChangeListener focusListener)231 public void setIconFocusListener(OnFocusChangeListener focusListener) { 232 mIconFocusListener = focusListener; 233 } 234 235 /** 236 * Sets the last search query that was made, used to show when there are no results and to also 237 * seed the intent for searching the market. 238 */ setLastSearchQuery(String query)239 public void setLastSearchQuery(String query) { 240 Resources res = mLauncher.getResources(); 241 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 242 mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); 243 } 244 245 /** 246 * Returns the grid layout manager. 247 */ getLayoutManager()248 public GridLayoutManager getLayoutManager() { 249 return mGridLayoutMgr; 250 } 251 252 @Override onCreateViewHolder(ViewGroup parent, int viewType)253 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 254 switch (viewType) { 255 case VIEW_TYPE_ICON: 256 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 257 R.layout.all_apps_icon, parent, false); 258 icon.setOnClickListener(mOnIconClickListener); 259 icon.setOnLongClickListener(mOnIconLongClickListener); 260 icon.setLongPressTimeoutFactor(1f); 261 icon.setOnFocusChangeListener(mIconFocusListener); 262 263 // Ensure the all apps icon height matches the workspace icons in portrait mode. 264 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; 265 return new ViewHolder(icon); 266 case VIEW_TYPE_EMPTY_SEARCH: 267 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 268 parent, false)); 269 case VIEW_TYPE_SEARCH_MARKET: 270 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 271 parent, false); 272 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely( 273 v, mMarketSearchIntent, null, AppLaunchTracker.CONTAINER_SEARCH)); 274 return new ViewHolder(searchMarketView); 275 case VIEW_TYPE_ALL_APPS_DIVIDER: 276 return new ViewHolder(mLayoutInflater.inflate( 277 R.layout.all_apps_divider, parent, false)); 278 default: 279 throw new RuntimeException("Unexpected view type"); 280 } 281 } 282 283 @Override onBindViewHolder(ViewHolder holder, int position)284 public void onBindViewHolder(ViewHolder holder, int position) { 285 switch (holder.getItemViewType()) { 286 case VIEW_TYPE_ICON: 287 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 288 BubbleTextView icon = (BubbleTextView) holder.itemView; 289 icon.reset(); 290 icon.applyFromApplicationInfo(info); 291 break; 292 case VIEW_TYPE_EMPTY_SEARCH: 293 TextView emptyViewText = (TextView) holder.itemView; 294 emptyViewText.setText(mEmptySearchMessage); 295 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 296 Gravity.START | Gravity.CENTER_VERTICAL); 297 break; 298 case VIEW_TYPE_SEARCH_MARKET: 299 TextView searchView = (TextView) holder.itemView; 300 if (mMarketSearchIntent != null) { 301 searchView.setVisibility(View.VISIBLE); 302 } else { 303 searchView.setVisibility(View.GONE); 304 } 305 break; 306 case VIEW_TYPE_ALL_APPS_DIVIDER: 307 // nothing to do 308 break; 309 } 310 } 311 312 @Override onFailedToRecycleView(ViewHolder holder)313 public boolean onFailedToRecycleView(ViewHolder holder) { 314 // Always recycle and we will reset the view when it is bound 315 return true; 316 } 317 318 @Override getItemCount()319 public int getItemCount() { 320 return mApps.getAdapterItems().size(); 321 } 322 323 @Override getItemViewType(int position)324 public int getItemViewType(int position) { 325 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 326 return item.viewType; 327 } 328 329 } 330