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.Intent; 20 import android.content.res.Resources; 21 import android.graphics.Point; 22 import android.support.v4.view.accessibility.AccessibilityEventCompat; 23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 24 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 25 import android.support.v7.widget.GridLayoutManager; 26 import android.support.v7.widget.RecyclerView; 27 import android.view.Gravity; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.View.OnFocusChangeListener; 31 import android.view.ViewConfiguration; 32 import android.view.ViewGroup; 33 import android.view.accessibility.AccessibilityEvent; 34 import android.widget.TextView; 35 36 import com.android.launcher3.discovery.AppDiscoveryAppInfo; 37 import com.android.launcher3.AppInfo; 38 import com.android.launcher3.BubbleTextView; 39 import com.android.launcher3.Launcher; 40 import com.android.launcher3.R; 41 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; 42 import com.android.launcher3.discovery.AppDiscoveryItemView; 43 44 import java.util.List; 45 46 /** 47 * The grid view adapter of all the apps. 48 */ 49 public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter.ViewHolder> { 50 51 public static final String TAG = "AppsGridAdapter"; 52 53 // A normal icon 54 public static final int VIEW_TYPE_ICON = 1 << 1; 55 // A prediction icon 56 public static final int VIEW_TYPE_PREDICTION_ICON = 1 << 2; 57 // The message shown when there are no filtered results 58 public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 3; 59 // The message to continue to a market search when there are no filtered results 60 public static final int VIEW_TYPE_SEARCH_MARKET = 1 << 4; 61 62 // We use various dividers for various purposes. They share enough attributes to reuse layouts, 63 // but differ in enough attributes to require different view types 64 65 // A divider that separates the apps list and the search market button 66 public static final int VIEW_TYPE_SEARCH_MARKET_DIVIDER = 1 << 5; 67 // The divider under the search field 68 public static final int VIEW_TYPE_SEARCH_DIVIDER = 1 << 6; 69 // The divider that separates prediction icons from the app list 70 public static final int VIEW_TYPE_PREDICTION_DIVIDER = 1 << 7; 71 public static final int VIEW_TYPE_APPS_LOADING_DIVIDER = 1 << 8; 72 public static final int VIEW_TYPE_DISCOVERY_ITEM = 1 << 9; 73 74 // Common view type masks 75 public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_SEARCH_DIVIDER 76 | VIEW_TYPE_SEARCH_MARKET_DIVIDER 77 | VIEW_TYPE_PREDICTION_DIVIDER; 78 public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON 79 | VIEW_TYPE_PREDICTION_ICON; 80 public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON 81 | VIEW_TYPE_DISCOVERY_ITEM; 82 83 84 public interface BindViewCallback { onBindView(ViewHolder holder)85 void onBindView(ViewHolder holder); 86 } 87 88 /** 89 * ViewHolder for each icon. 90 */ 91 public static class ViewHolder extends RecyclerView.ViewHolder { ViewHolder(View v)92 public ViewHolder(View v) { 93 super(v); 94 } 95 } 96 97 /** 98 * A subclass of GridLayoutManager that overrides accessibility values during app search. 99 */ 100 public class AppsGridLayoutManager extends GridLayoutManager { 101 AppsGridLayoutManager(Context context)102 public AppsGridLayoutManager(Context context) { 103 super(context, 1, GridLayoutManager.VERTICAL, false); 104 } 105 106 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)107 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 108 super.onInitializeAccessibilityEvent(event); 109 110 // Ensure that we only report the number apps for accessibility not including other 111 // adapter views 112 final AccessibilityRecordCompat record = AccessibilityEventCompat 113 .asRecord(event); 114 record.setItemCount(mApps.getNumFilteredApps()); 115 record.setFromIndex(Math.max(0, 116 record.getFromIndex() - getRowsNotForAccessibility(record.getFromIndex()))); 117 record.setToIndex(Math.max(0, 118 record.getToIndex() - getRowsNotForAccessibility(record.getToIndex()))); 119 } 120 121 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)122 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 123 RecyclerView.State state) { 124 return super.getRowCountForAccessibility(recycler, state) - 125 getRowsNotForAccessibility(mApps.getAdapterItems().size() - 1); 126 } 127 128 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)129 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 130 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 131 super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info); 132 133 ViewGroup.LayoutParams lp = host.getLayoutParams(); 134 AccessibilityNodeInfoCompat.CollectionItemInfoCompat cic = info.getCollectionItemInfo(); 135 if (!(lp instanceof LayoutParams) || (cic == null)) { 136 return; 137 } 138 LayoutParams glp = (LayoutParams) lp; 139 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 140 cic.getRowIndex() - getRowsNotForAccessibility(glp.getViewAdapterPosition()), 141 cic.getRowSpan(), 142 cic.getColumnIndex(), 143 cic.getColumnSpan(), 144 cic.isHeading(), 145 cic.isSelected())); 146 } 147 148 /** 149 * Returns the number of rows before {@param adapterPosition}, including this position 150 * which should not be counted towards the collection info. 151 */ getRowsNotForAccessibility(int adapterPosition)152 private int getRowsNotForAccessibility(int adapterPosition) { 153 List<AdapterItem> items = mApps.getAdapterItems(); 154 adapterPosition = Math.max(adapterPosition, mApps.getAdapterItems().size() - 1); 155 int extraRows = 0; 156 for (int i = 0; i <= adapterPosition; i++) { 157 if (!isViewType(items.get(i).viewType, VIEW_TYPE_MASK_CONTENT)) { 158 extraRows++; 159 } 160 } 161 return extraRows; 162 } 163 164 @Override getPaddingBottom()165 public int getPaddingBottom() { 166 return mLauncher.getDragLayer().getInsets().bottom; 167 } 168 } 169 170 /** 171 * Helper class to size the grid items. 172 */ 173 public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup { 174 GridSpanSizer()175 public GridSpanSizer() { 176 super(); 177 setSpanIndexCacheEnabled(true); 178 } 179 180 @Override getSpanSize(int position)181 public int getSpanSize(int position) { 182 if (isIconViewType(mApps.getAdapterItems().get(position).viewType)) { 183 return 1; 184 } else { 185 // Section breaks span the full width 186 return mAppsPerRow; 187 } 188 } 189 } 190 191 private final Launcher mLauncher; 192 private final LayoutInflater mLayoutInflater; 193 private final AlphabeticalAppsList mApps; 194 private final GridLayoutManager mGridLayoutMgr; 195 private final GridSpanSizer mGridSizer; 196 private final View.OnClickListener mIconClickListener; 197 private final View.OnLongClickListener mIconLongClickListener; 198 199 private int mAppsPerRow; 200 201 private BindViewCallback mBindViewCallback; 202 private AllAppsSearchBarController mSearchController; 203 private OnFocusChangeListener mIconFocusListener; 204 205 // The text to show when there are no search results and no market search handler. 206 private String mEmptySearchMessage; 207 // The intent to send off to the market app, updated each time the search query changes. 208 private Intent mMarketSearchIntent; 209 AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener)210 public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener 211 iconClickListener, View.OnLongClickListener iconLongClickListener) { 212 Resources res = launcher.getResources(); 213 mLauncher = launcher; 214 mApps = apps; 215 mEmptySearchMessage = res.getString(R.string.all_apps_loading_message); 216 mGridSizer = new GridSpanSizer(); 217 mGridLayoutMgr = new AppsGridLayoutManager(launcher); 218 mGridLayoutMgr.setSpanSizeLookup(mGridSizer); 219 mLayoutInflater = LayoutInflater.from(launcher); 220 mIconClickListener = iconClickListener; 221 mIconLongClickListener = iconLongClickListener; 222 } 223 isDividerViewType(int viewType)224 public static boolean isDividerViewType(int viewType) { 225 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 226 } 227 isIconViewType(int viewType)228 public static boolean isIconViewType(int viewType) { 229 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 230 } 231 isViewType(int viewType, int viewTypeMask)232 public static boolean isViewType(int viewType, int viewTypeMask) { 233 return (viewType & viewTypeMask) != 0; 234 } 235 236 /** 237 * Sets the number of apps per row. 238 */ setNumAppsPerRow(int appsPerRow)239 public void setNumAppsPerRow(int appsPerRow) { 240 mAppsPerRow = appsPerRow; 241 mGridLayoutMgr.setSpanCount(appsPerRow); 242 } 243 setSearchController(AllAppsSearchBarController searchController)244 public void setSearchController(AllAppsSearchBarController searchController) { 245 mSearchController = searchController; 246 } 247 setIconFocusListener(OnFocusChangeListener focusListener)248 public void setIconFocusListener(OnFocusChangeListener focusListener) { 249 mIconFocusListener = focusListener; 250 } 251 252 /** 253 * Sets the last search query that was made, used to show when there are no results and to also 254 * seed the intent for searching the market. 255 */ setLastSearchQuery(String query)256 public void setLastSearchQuery(String query) { 257 Resources res = mLauncher.getResources(); 258 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 259 mMarketSearchIntent = mSearchController.createMarketSearchIntent(query); 260 } 261 262 /** 263 * Sets the callback for when views are bound. 264 */ setBindViewCallback(BindViewCallback cb)265 public void setBindViewCallback(BindViewCallback cb) { 266 mBindViewCallback = cb; 267 } 268 269 /** 270 * Returns the grid layout manager. 271 */ getLayoutManager()272 public GridLayoutManager getLayoutManager() { 273 return mGridLayoutMgr; 274 } 275 276 @Override onCreateViewHolder(ViewGroup parent, int viewType)277 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 278 switch (viewType) { 279 case VIEW_TYPE_ICON: 280 case VIEW_TYPE_PREDICTION_ICON: 281 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 282 R.layout.all_apps_icon, parent, false); 283 icon.setOnClickListener(mIconClickListener); 284 icon.setOnLongClickListener(mIconLongClickListener); 285 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext()) 286 .getLongPressTimeout()); 287 icon.setOnFocusChangeListener(mIconFocusListener); 288 289 // Ensure the all apps icon height matches the workspace icons 290 icon.getLayoutParams().height = getCellSize().y; 291 return new ViewHolder(icon); 292 case VIEW_TYPE_DISCOVERY_ITEM: 293 AppDiscoveryItemView appDiscoveryItemView = (AppDiscoveryItemView) mLayoutInflater 294 .inflate(R.layout.all_apps_discovery_item, parent, false); 295 appDiscoveryItemView.init(mIconClickListener, mLauncher.getAccessibilityDelegate(), 296 mIconLongClickListener); 297 return new ViewHolder(appDiscoveryItemView); 298 case VIEW_TYPE_EMPTY_SEARCH: 299 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 300 parent, false)); 301 case VIEW_TYPE_SEARCH_MARKET: 302 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 303 parent, false); 304 searchMarketView.setOnClickListener(new View.OnClickListener() { 305 @Override 306 public void onClick(View v) { 307 mLauncher.startActivitySafely(v, mMarketSearchIntent, null); 308 } 309 }); 310 return new ViewHolder(searchMarketView); 311 case VIEW_TYPE_SEARCH_DIVIDER: 312 return new ViewHolder(mLayoutInflater.inflate( 313 R.layout.all_apps_search_divider, parent, false)); 314 case VIEW_TYPE_APPS_LOADING_DIVIDER: 315 View loadingDividerView = mLayoutInflater.inflate( 316 R.layout.all_apps_discovery_loading_divider, parent, false); 317 return new ViewHolder(loadingDividerView); 318 case VIEW_TYPE_PREDICTION_DIVIDER: 319 case VIEW_TYPE_SEARCH_MARKET_DIVIDER: 320 return new ViewHolder(mLayoutInflater.inflate( 321 R.layout.all_apps_divider, parent, false)); 322 default: 323 throw new RuntimeException("Unexpected view type"); 324 } 325 } 326 getCellSize()327 private Point getCellSize() { 328 return mLauncher.getDeviceProfile().getCellSize(); 329 } 330 331 @Override onBindViewHolder(ViewHolder holder, int position)332 public void onBindViewHolder(ViewHolder holder, int position) { 333 switch (holder.getItemViewType()) { 334 case VIEW_TYPE_ICON: 335 case VIEW_TYPE_PREDICTION_ICON: 336 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 337 BubbleTextView icon = (BubbleTextView) holder.itemView; 338 icon.applyFromApplicationInfo(info); 339 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); 340 break; 341 case VIEW_TYPE_DISCOVERY_ITEM: 342 AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo) 343 mApps.getAdapterItems().get(position).appInfo; 344 AppDiscoveryItemView view = (AppDiscoveryItemView) holder.itemView; 345 view.apply(appDiscoveryAppInfo); 346 break; 347 case VIEW_TYPE_EMPTY_SEARCH: 348 TextView emptyViewText = (TextView) holder.itemView; 349 emptyViewText.setText(mEmptySearchMessage); 350 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 351 Gravity.START | Gravity.CENTER_VERTICAL); 352 break; 353 case VIEW_TYPE_SEARCH_MARKET: 354 TextView searchView = (TextView) holder.itemView; 355 if (mMarketSearchIntent != null) { 356 searchView.setVisibility(View.VISIBLE); 357 } else { 358 searchView.setVisibility(View.GONE); 359 } 360 break; 361 case VIEW_TYPE_APPS_LOADING_DIVIDER: 362 int visLoading = mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; 363 int visLoaded = !mApps.isAppDiscoveryRunning() ? View.VISIBLE : View.GONE; 364 holder.itemView.findViewById(R.id.loadingProgressBar).setVisibility(visLoading); 365 holder.itemView.findViewById(R.id.loadedDivider).setVisibility(visLoaded); 366 break; 367 case VIEW_TYPE_SEARCH_MARKET_DIVIDER: 368 // nothing to do 369 break; 370 } 371 if (mBindViewCallback != null) { 372 mBindViewCallback.onBindView(holder); 373 } 374 } 375 376 @Override onFailedToRecycleView(ViewHolder holder)377 public boolean onFailedToRecycleView(ViewHolder holder) { 378 // Always recycle and we will reset the view when it is bound 379 return true; 380 } 381 382 @Override getItemCount()383 public int getItemCount() { 384 return mApps.getAdapterItems().size(); 385 } 386 387 @Override getItemViewType(int position)388 public int getItemViewType(int position) { 389 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 390 return item.viewType; 391 } 392 } 393