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