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 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 setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns); 204 } 205 setAppsPerRow(int appsPerRow)206 public void setAppsPerRow(int appsPerRow) { 207 mAppsPerRow = appsPerRow; 208 mGridLayoutMgr.setSpanCount(mAppsPerRow); 209 } 210 isDividerViewType(int viewType)211 public static boolean isDividerViewType(int viewType) { 212 return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER); 213 } 214 isIconViewType(int viewType)215 public static boolean isIconViewType(int viewType) { 216 return isViewType(viewType, VIEW_TYPE_MASK_ICON); 217 } 218 isViewType(int viewType, int viewTypeMask)219 public static boolean isViewType(int viewType, int viewTypeMask) { 220 return (viewType & viewTypeMask) != 0; 221 } 222 setIconFocusListener(OnFocusChangeListener focusListener)223 public void setIconFocusListener(OnFocusChangeListener focusListener) { 224 mIconFocusListener = focusListener; 225 } 226 227 /** 228 * Sets the last search query that was made, used to show when there are no results and to also 229 * seed the intent for searching the market. 230 */ setLastSearchQuery(String query)231 public void setLastSearchQuery(String query) { 232 Resources res = mLauncher.getResources(); 233 mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query); 234 mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query); 235 } 236 237 /** 238 * Sets the callback for when views are bound. 239 */ setBindViewCallback(BindViewCallback cb)240 public void setBindViewCallback(BindViewCallback cb) { 241 mBindViewCallback = cb; 242 } 243 244 /** 245 * Returns the grid layout manager. 246 */ getLayoutManager()247 public GridLayoutManager getLayoutManager() { 248 return mGridLayoutMgr; 249 } 250 251 @Override onCreateViewHolder(ViewGroup parent, int viewType)252 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 253 switch (viewType) { 254 case VIEW_TYPE_ICON: 255 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate( 256 R.layout.all_apps_icon, parent, false); 257 icon.setOnClickListener(ItemClickHandler.INSTANCE); 258 icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS); 259 icon.setLongPressTimeoutFactor(1f); 260 icon.setOnFocusChangeListener(mIconFocusListener); 261 262 // Ensure the all apps icon height matches the workspace icons in portrait mode. 263 icon.getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; 264 return new ViewHolder(icon); 265 case VIEW_TYPE_EMPTY_SEARCH: 266 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, 267 parent, false)); 268 case VIEW_TYPE_SEARCH_MARKET: 269 View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market, 270 parent, false); 271 searchMarketView.setOnClickListener(v -> mLauncher.startActivitySafely( 272 v, mMarketSearchIntent, null, AppLaunchTracker.CONTAINER_SEARCH)); 273 return new ViewHolder(searchMarketView); 274 case VIEW_TYPE_ALL_APPS_DIVIDER: 275 return new ViewHolder(mLayoutInflater.inflate( 276 R.layout.all_apps_divider, parent, false)); 277 case VIEW_TYPE_WORK_TAB_FOOTER: 278 View footer = mLayoutInflater.inflate(R.layout.work_tab_footer, parent, false); 279 return new ViewHolder(footer); 280 default: 281 throw new RuntimeException("Unexpected view type"); 282 } 283 } 284 285 @Override onBindViewHolder(ViewHolder holder, int position)286 public void onBindViewHolder(ViewHolder holder, int position) { 287 switch (holder.getItemViewType()) { 288 case VIEW_TYPE_ICON: 289 AppInfo info = mApps.getAdapterItems().get(position).appInfo; 290 BubbleTextView icon = (BubbleTextView) holder.itemView; 291 icon.reset(); 292 icon.applyFromApplicationInfo(info); 293 break; 294 case VIEW_TYPE_EMPTY_SEARCH: 295 TextView emptyViewText = (TextView) holder.itemView; 296 emptyViewText.setText(mEmptySearchMessage); 297 emptyViewText.setGravity(mApps.hasNoFilteredResults() ? Gravity.CENTER : 298 Gravity.START | Gravity.CENTER_VERTICAL); 299 break; 300 case VIEW_TYPE_SEARCH_MARKET: 301 TextView searchView = (TextView) holder.itemView; 302 if (mMarketSearchIntent != null) { 303 searchView.setVisibility(View.VISIBLE); 304 } else { 305 searchView.setVisibility(View.GONE); 306 } 307 break; 308 case VIEW_TYPE_ALL_APPS_DIVIDER: 309 // nothing to do 310 break; 311 case VIEW_TYPE_WORK_TAB_FOOTER: 312 WorkModeSwitch workModeToggle = holder.itemView.findViewById(R.id.work_mode_toggle); 313 workModeToggle.refresh(); 314 TextView managedByLabel = holder.itemView.findViewById(R.id.managed_by_label); 315 boolean anyProfileQuietModeEnabled = UserManagerCompat.getInstance( 316 managedByLabel.getContext()).isAnyProfileQuietModeEnabled(); 317 managedByLabel.setText(anyProfileQuietModeEnabled 318 ? R.string.work_mode_off_label : R.string.work_mode_on_label); 319 break; 320 } 321 if (mBindViewCallback != null) { 322 mBindViewCallback.onBindView(holder); 323 } 324 } 325 326 @Override onFailedToRecycleView(ViewHolder holder)327 public boolean onFailedToRecycleView(ViewHolder holder) { 328 // Always recycle and we will reset the view when it is bound 329 return true; 330 } 331 332 @Override getItemCount()333 public int getItemCount() { 334 return mApps.getAdapterItems().size(); 335 } 336 337 @Override getItemViewType(int position)338 public int getItemViewType(int position) { 339 AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position); 340 return item.viewType; 341 } 342 343 } 344