• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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