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