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