1 /* 2 * Copyright (C) 2023 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 17 package com.android.car.carlauncher.recyclerview; 18 19 import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection; 20 import static com.android.car.carlauncher.AppGridConstants.PageOrientation; 21 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.LinearLayout; 28 29 import androidx.recyclerview.widget.DiffUtil; 30 import androidx.recyclerview.widget.RecyclerView; 31 32 import com.android.car.carlauncher.AppGridActivity.Mode; 33 import com.android.car.carlauncher.AppGridPageSnapper; 34 import com.android.car.carlauncher.AppItem; 35 import com.android.car.carlauncher.LauncherItem; 36 import com.android.car.carlauncher.LauncherItemDiffCallback; 37 import com.android.car.carlauncher.LauncherViewModel; 38 import com.android.car.carlauncher.R; 39 import com.android.car.carlauncher.RecentAppsRowViewHolder; 40 import com.android.car.carlauncher.pagination.PageIndexingHelper; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 45 /** 46 * The adapter that populates the grid view with apps. 47 */ 48 public class AppGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 49 public static final int RECENT_APPS_TYPE = 1; 50 public static final int APP_ITEM_TYPE = 2; 51 52 private static final String TAG = "AppGridAdapter"; 53 private final Context mContext; 54 private final LayoutInflater mInflater; 55 private final PageIndexingHelper mIndexingHelper; 56 private final AppItemViewHolder.AppItemDragCallback mDragCallback; 57 private final AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback; 58 private final int mNumOfCols; 59 private final int mNumOfRows; 60 private int mAppItemWidth; 61 private int mAppItemHeight; 62 // grid order of the mLauncherItems used by DiffUtils in dispatchUpdates to animate UI updates 63 private final List<LauncherItem> mGridOrderedLauncherItems; 64 65 private List<LauncherItem> mLauncherItems; 66 private boolean mIsDistractionOptimizationRequired; 67 private int mPageScrollDestination; 68 // the global bounding rect of the app grid including margins (excluding page indicator bar) 69 private Rect mPageBound; 70 private Mode mAppGridMode; 71 72 private AppGridAdapterListener mAppGridAdapterListener; 73 AppGridAdapter(Context context, int numOfCols, int numOfRows, LauncherViewModel launcherViewModel, AppItemViewHolder.AppItemDragCallback dragCallback, AppGridPageSnapper.AppGridPageSnapCallback snapCallback)74 public AppGridAdapter(Context context, int numOfCols, int numOfRows, 75 LauncherViewModel launcherViewModel, AppItemViewHolder.AppItemDragCallback dragCallback, 76 AppGridPageSnapper.AppGridPageSnapCallback snapCallback) { 77 this(context, numOfCols, numOfRows, 78 context.getResources().getBoolean(R.bool.use_vertical_app_grid) 79 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL, 80 LayoutInflater.from(context), launcherViewModel, dragCallback, snapCallback); 81 } 82 AppGridAdapter(Context context, int numOfCols, int numOfRows, @PageOrientation int pageOrientation, LayoutInflater layoutInflater, LauncherViewModel launcherViewModel, AppItemViewHolder.AppItemDragCallback dragCallback, AppGridPageSnapper.AppGridPageSnapCallback snapCallback)83 public AppGridAdapter(Context context, int numOfCols, int numOfRows, 84 @PageOrientation int pageOrientation, 85 LayoutInflater layoutInflater, LauncherViewModel launcherViewModel, 86 AppItemViewHolder.AppItemDragCallback dragCallback, 87 AppGridPageSnapper.AppGridPageSnapCallback snapCallback) { 88 this(context, numOfCols, numOfRows, pageOrientation, layoutInflater, 89 launcherViewModel, dragCallback, snapCallback, Mode.ALL_APPS); 90 } 91 AppGridAdapter(Context context, int numOfCols, int numOfRows, @PageOrientation int pageOrientation, LayoutInflater layoutInflater, LauncherViewModel launcherViewModel, AppItemViewHolder.AppItemDragCallback dragCallback, AppGridPageSnapper.AppGridPageSnapCallback snapCallback, Mode mode)92 public AppGridAdapter(Context context, int numOfCols, int numOfRows, 93 @PageOrientation int pageOrientation, 94 LayoutInflater layoutInflater, LauncherViewModel launcherViewModel, 95 AppItemViewHolder.AppItemDragCallback dragCallback, 96 AppGridPageSnapper.AppGridPageSnapCallback snapCallback, Mode mode) { 97 mContext = context; 98 mInflater = layoutInflater; 99 mNumOfCols = numOfCols; 100 mNumOfRows = numOfRows; 101 mDragCallback = dragCallback; 102 mSnapCallback = snapCallback; 103 104 mIndexingHelper = new PageIndexingHelper(numOfCols, numOfRows, pageOrientation); 105 mGridOrderedLauncherItems = new ArrayList<>(); 106 mAppGridMode = mode; 107 } 108 AppGridAdapter(Context context, int numOfCols, int numOfRows, AppItemViewHolder.AppItemDragCallback dragCallback, AppGridPageSnapper.AppGridPageSnapCallback snapCallback, AppGridAdapterListener appGridAdapterListener, Mode mode)109 public AppGridAdapter(Context context, int numOfCols, int numOfRows, 110 AppItemViewHolder.AppItemDragCallback dragCallback, 111 AppGridPageSnapper.AppGridPageSnapCallback snapCallback, 112 AppGridAdapterListener appGridAdapterListener, 113 Mode mode) { 114 mContext = context; 115 mInflater = LayoutInflater.from(context); 116 mNumOfCols = numOfCols; 117 mNumOfRows = numOfRows; 118 mDragCallback = dragCallback; 119 mSnapCallback = snapCallback; 120 int pageOrientation = context.getResources().getBoolean(R.bool.use_vertical_app_grid) 121 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 122 mIndexingHelper = new PageIndexingHelper(numOfCols, numOfRows, pageOrientation); 123 mGridOrderedLauncherItems = new ArrayList<>(); 124 mAppGridMode = mode; 125 mAppGridAdapterListener = appGridAdapterListener; 126 } 127 128 /** 129 * Updates the dimension measurements of the app items and app grid bounds. 130 * 131 * To dispatch the UI changes, the recyclerview needs to call {@link RecyclerView#setAdapter} 132 * after calling this method to recreate the view holders. 133 */ updateViewHolderDimensions(Rect pageBound, int appItemWidth, int appItemHeight)134 public void updateViewHolderDimensions(Rect pageBound, int appItemWidth, int appItemHeight) { 135 mPageBound = pageBound; 136 mAppItemWidth = appItemWidth; 137 mAppItemHeight = appItemHeight; 138 } 139 140 /** 141 * Updates the current driving restriction to {@code isDistractionOptimizationRequired}, then 142 * rebind the view holders. 143 */ setIsDistractionOptimizationRequired(boolean isDistractionOptimizationRequired)144 public void setIsDistractionOptimizationRequired(boolean isDistractionOptimizationRequired) { 145 mIsDistractionOptimizationRequired = isDistractionOptimizationRequired; 146 // notifyDataSetChanged will rebind distraction optimization to all app items 147 notifyDataSetChanged(); 148 } 149 150 /** 151 * Updates the current app grid mode to {@code mode}, then 152 * rebind the view holders. 153 */ setMode(Mode mode)154 public void setMode(Mode mode) { 155 mAppGridMode = mode; 156 notifyDataSetChanged(); 157 } 158 159 /** 160 * Sets a new list of launcher items to be displayed in the app grid. 161 * This should only be called by onChanged() in the observer as a response to data change in the 162 * adapter's LauncherViewModel. 163 */ setLauncherItems(List<? extends LauncherItem> launcherItems)164 public void setLauncherItems(List<? extends LauncherItem> launcherItems) { 165 mLauncherItems = (List<LauncherItem>) launcherItems; 166 int newSnapPosition = mSnapCallback.getSnapPosition(); 167 if (newSnapPosition != 0 && newSnapPosition >= getItemCount()) { 168 // in case user deletes the only app item on the last page, the page should snap to the 169 // last icon on the second last page. 170 mSnapCallback.notifySnapToPosition(getItemCount() - 1); 171 } 172 dispatchUpdates(); 173 } 174 175 @Override getItemViewType(int position)176 public int getItemViewType(int position) { 177 if (position == 0 && hasRecentlyUsedApps()) { 178 return RECENT_APPS_TYPE; 179 } 180 return APP_ITEM_TYPE; 181 } 182 183 @Override onCreateViewHolder(ViewGroup parent, int viewType)184 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 185 if (viewType == RECENT_APPS_TYPE) { 186 View view = 187 mInflater.inflate(R.layout.recent_apps_row, parent, /* attachToRoot= */ false); 188 return new RecentAppsRowViewHolder(view, mContext); 189 } else { 190 View view = mInflater.inflate(R.layout.app_item, parent, /* attachToRoot= */ false); 191 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 192 mAppItemWidth, mAppItemHeight); 193 view.setLayoutParams(layoutParams); 194 return new AppItemViewHolder(view, mContext, mDragCallback, mSnapCallback); 195 } 196 } 197 198 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)199 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 200 AppItemViewHolder viewHolder = (AppItemViewHolder) holder; 201 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( 202 mAppItemWidth, mAppItemHeight); 203 holder.itemView.setLayoutParams(layoutParams); 204 205 AppItemViewHolder.BindInfo bindInfo = new AppItemViewHolder.BindInfo( 206 mIsDistractionOptimizationRequired, mPageBound, mAppGridMode); 207 int adapterIndex = mIndexingHelper.gridPositionToAdaptorIndex(position); 208 if (adapterIndex >= mLauncherItems.size()) { 209 // the current view holder is an empty item used to pad the last page. 210 viewHolder.bind(null, bindInfo); 211 return; 212 } 213 AppItem item = (AppItem) mLauncherItems.get(adapterIndex); 214 viewHolder.bind(item.getAppMetaData(), bindInfo); 215 } 216 217 /** 218 * Sets the layout direction of the indexing helper. 219 */ setLayoutDirection(int layoutDirection)220 public void setLayoutDirection(int layoutDirection) { 221 mIndexingHelper.setLayoutDirection(layoutDirection); 222 } 223 224 @Override getItemCount()225 public int getItemCount() { 226 return getItemCountInternal(getLauncherItemsCount()); 227 } 228 229 /** Returns the item count including padded spaces on the last page */ getItemCountInternal(int unpaddedItemCount)230 private int getItemCountInternal(int unpaddedItemCount) { 231 // item count should always be a multiple of block size to ensure pagination 232 // is done properly. Extra spaces will have empty ViewHolders binded. 233 float pageFraction = (float) unpaddedItemCount / (mNumOfCols * mNumOfRows); 234 int pageCount = (int) Math.ceil(pageFraction); 235 return pageCount * mNumOfCols * mNumOfRows; 236 } 237 getLauncherItemsCount()238 public int getLauncherItemsCount() { 239 return mLauncherItems == null ? 0 : mLauncherItems.size(); 240 } 241 242 /** 243 * Calculates the number of pages required to fit the all app items in the recycler view, with 244 * minimum of 1 page when no items have been added to data model. 245 */ getPageCount()246 public int getPageCount() { 247 return getPageCount(/* unpaddedItemCount */ getItemCount()); 248 } 249 250 /** 251 * Calculates the number of pages required to fit {@code unpaddedItemCount} number of app items. 252 */ getPageCount(int unpaddedItemCount)253 public int getPageCount(int unpaddedItemCount) { 254 int pageCount = getItemCountInternal(unpaddedItemCount) / (mNumOfRows * mNumOfCols); 255 return Math.max(pageCount, 1); 256 } 257 258 /** 259 * Return the offset bound direction of the given gridPosition. 260 */ 261 @AppItemBoundDirection getOffsetBoundDirection(int gridPosition)262 public int getOffsetBoundDirection(int gridPosition) { 263 return mIndexingHelper.getOffsetBoundDirection(gridPosition); 264 } 265 266 hasRecentlyUsedApps()267 private boolean hasRecentlyUsedApps() { 268 // TODO (b/266988404): deprecate ui logic associated with recently used apps 269 return false; 270 } 271 272 /** 273 * Sets the cached drag start position to {@code gridPosition}. 274 */ setDragStartPoint(int gridPosition)275 public void setDragStartPoint(int gridPosition) { 276 mPageScrollDestination = mIndexingHelper.roundToFirstIndexOnPage(gridPosition); 277 mSnapCallback.notifySnapToPosition(mPageScrollDestination); 278 } 279 280 /** 281 * The magical function that writes the new order to proto datastore. 282 * 283 * There should not be any calls to update RecyclerView, such as via notifyDatasetChanged in 284 * this method since UI changes relating to data model should be handled by data observer. 285 */ moveAppItem(int gridPositionFrom, int gridPositionTo)286 public void moveAppItem(int gridPositionFrom, int gridPositionTo) { 287 int adaptorIndexFrom = mIndexingHelper.gridPositionToAdaptorIndex(gridPositionFrom); 288 int adaptorIndexTo = mIndexingHelper.gridPositionToAdaptorIndex(gridPositionTo); 289 mPageScrollDestination = mIndexingHelper.roundToFirstIndexOnPage(gridPositionTo); 290 mSnapCallback.notifySnapToPosition(mPageScrollDestination); 291 292 // we need to move package to target index even if the from and to index are the same to 293 // ensure dispatchLayout gets called to re-anchor the recyclerview to current page. 294 AppItem selectedApp = (AppItem) mLauncherItems.get(adaptorIndexFrom); 295 mAppGridAdapterListener.onAppPositionChanged(adaptorIndexTo, selectedApp); 296 } 297 298 299 /** 300 * Updates page scroll destination after user has held the app item at the end of page for 301 * longer than the scroll dispatch threshold. 302 */ updatePageScrollDestination(boolean scrollToNextPage)303 public void updatePageScrollDestination(boolean scrollToNextPage) { 304 int newDestination; 305 int blockSize = mNumOfCols * mNumOfRows; 306 if (scrollToNextPage) { 307 newDestination = mPageScrollDestination + blockSize; 308 mPageScrollDestination = (newDestination >= getItemCount()) ? mPageScrollDestination : 309 mIndexingHelper.roundToLastIndexOnPage(newDestination); 310 } else { 311 newDestination = mPageScrollDestination - blockSize; 312 mPageScrollDestination = (newDestination < 0) ? mPageScrollDestination : 313 mIndexingHelper.roundToFirstIndexOnPage(newDestination); 314 } 315 mSnapCallback.notifySnapToPosition(mPageScrollDestination); 316 } 317 318 /** 319 * Returns the last cached page scroll destination. 320 */ getPageScrollDestination()321 public int getPageScrollDestination() { 322 return mPageScrollDestination; 323 } 324 325 /** 326 * Dispatches the paged reordering animation using async list differ, based on 327 * the current adapter order when the method is called. 328 */ dispatchUpdates()329 private void dispatchUpdates() { 330 List<LauncherItem> newAppsList = new ArrayList<>(); 331 // we first need to pad the empty items on the last page 332 for (int i = 0; i < getItemCount(); i++) { 333 newAppsList.add(getEmptyLauncherItem()); 334 } 335 336 for (int i = 0; i < mLauncherItems.size(); i++) { 337 newAppsList.set(mIndexingHelper.adaptorIndexToGridPosition(i), mLauncherItems.get(i)); 338 } 339 LauncherItemDiffCallback callback = new LauncherItemDiffCallback( 340 /* oldList */ mGridOrderedLauncherItems, /* newList */ newAppsList); 341 DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback); 342 343 mGridOrderedLauncherItems.clear(); 344 mGridOrderedLauncherItems.addAll(newAppsList); 345 result.dispatchUpdatesTo(this); 346 } 347 getEmptyLauncherItem()348 private LauncherItem getEmptyLauncherItem() { 349 return new AppItem(/* packageName*/ "", /* className */ "", /* displayName */ "", 350 /* appMetaData */ null); 351 } 352 353 /** 354 * Returns the grid position of the next intended rotary focus view. This should follow the 355 * same logical order as the adapter indexes. 356 */ getNextRotaryFocus(int focusedGridPosition, int direction)357 public int getNextRotaryFocus(int focusedGridPosition, int direction) { 358 int targetAdapterIndex = mIndexingHelper.gridPositionToAdaptorIndex(focusedGridPosition) 359 + (direction == View.FOCUS_FORWARD ? 1 : -1); 360 if (targetAdapterIndex < 0 || targetAdapterIndex >= getLauncherItemsCount()) { 361 return focusedGridPosition; 362 } 363 return mIndexingHelper.adaptorIndexToGridPosition(targetAdapterIndex); 364 } 365 366 public interface AppGridAdapterListener { onAppPositionChanged(int newPosition, AppItem appItem)367 void onAppPositionChanged(int newPosition, AppItem appItem); 368 } 369 } 370