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