1 /*
2  * Copyright (C) 2022 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 static android.view.View.GONE;
19 
20 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
21 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
22 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
23 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_LEFT;
24 import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_RIGHT;
25 import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
26 import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
27 
28 import android.content.Context;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.View.OnClickListener;
32 import android.view.View.OnFocusChangeListener;
33 import android.view.View.OnLongClickListener;
34 import android.view.ViewGroup;
35 import android.widget.RelativeLayout;
36 import android.widget.TextView;
37 
38 import androidx.annotation.Nullable;
39 import androidx.recyclerview.widget.RecyclerView;
40 
41 import com.android.launcher3.BubbleTextView;
42 import com.android.launcher3.Flags;
43 import com.android.launcher3.LauncherPrefs;
44 import com.android.launcher3.R;
45 import com.android.launcher3.allapps.search.SearchAdapterProvider;
46 import com.android.launcher3.model.data.AppInfo;
47 import com.android.launcher3.views.ActivityContext;
48 
49 /**
50  * Adapter for all the apps.
51  *
52  * @param <T> Type of context inflating all apps.
53  */
54 public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> extends
55         RecyclerView.Adapter<BaseAllAppsAdapter.ViewHolder> {
56 
57     public static final String TAG = "BaseAllAppsAdapter";
58 
59     // A normal icon
60     public static final int VIEW_TYPE_ICON = 1 << 1;
61     // The message shown when there are no filtered results
62     public static final int VIEW_TYPE_EMPTY_SEARCH = 1 << 2;
63     // A divider that separates the apps list and the search market button
64     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 3;
65 
66     public static final int VIEW_TYPE_WORK_EDU_CARD = 1 << 4;
67     public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5;
68     public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6;
69     public static final int VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER = 1 << 7;
70     public static final int NEXT_ID = 8;
71 
72     // Common view type masks
73     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
74     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
75 
76     public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER =
77             VIEW_TYPE_PRIVATE_SPACE_HEADER;
78     public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_SYS_APPS_DIVIDER =
79             VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER;
80 
81     protected final SearchAdapterProvider<?> mAdapterProvider;
82 
83     /**
84      * ViewHolder for each icon.
85      */
86     public static class ViewHolder extends RecyclerView.ViewHolder {
87 
ViewHolder(View v)88         public ViewHolder(View v) {
89             super(v);
90         }
91     }
92 
93     /** Sets the number of apps to be displayed in one row of the all apps screen. */
setAppsPerRow(int appsPerRow)94     public abstract void setAppsPerRow(int appsPerRow);
95 
96     /**
97      * Info about a particular adapter item (can be either section or app)
98      */
99     public static class AdapterItem {
100         /** Common properties */
101         // The type of this item
102         public final int viewType;
103 
104         // The row that this item shows up on
105         public int rowIndex;
106         // The index of this app in the row
107         public int rowAppIndex;
108         // The associated ItemInfoWithIcon for the item
109         public AppInfo itemInfo = null;
110         // Private App Decorator
111         public SectionDecorationInfo decorationInfo = null;
AdapterItem(int viewType)112         public AdapterItem(int viewType) {
113             this.viewType = viewType;
114         }
115 
116         /**
117          * Factory method for AppIcon AdapterItem
118          */
asApp(AppInfo appInfo)119         public static AdapterItem asApp(AppInfo appInfo) {
120             AdapterItem item = new AdapterItem(VIEW_TYPE_ICON);
121             item.itemInfo = appInfo;
122             return item;
123         }
124 
asAppWithDecorationInfo(AppInfo appInfo, SectionDecorationInfo decorationInfo)125         public static AdapterItem asAppWithDecorationInfo(AppInfo appInfo,
126                 SectionDecorationInfo decorationInfo) {
127             AdapterItem item = asApp(appInfo);
128             item.decorationInfo = decorationInfo;
129             return item;
130         }
131 
isCountedForAccessibility()132         protected boolean isCountedForAccessibility() {
133             return viewType == VIEW_TYPE_ICON;
134         }
135 
136         /**
137          * Returns true if the items represent the same object
138          */
isSameAs(AdapterItem other)139         public boolean isSameAs(AdapterItem other) {
140             return (other.viewType == viewType) && (other.getClass() == getClass());
141         }
142 
143         /**
144          * This is called only if {@link #isSameAs} returns true to check if the contents are same
145          * as well. Returning true will prevent redrawing of thee item.
146          */
isContentSame(AdapterItem other)147         public boolean isContentSame(AdapterItem other) {
148             return itemInfo == null && other.itemInfo == null;
149         }
150 
151         @Nullable
getDecorationInfo()152         public SectionDecorationInfo getDecorationInfo() {
153             return decorationInfo;
154         }
155 
156         /** Sets the alpha of the decorator for this item. */
setDecorationFillAlpha(int alpha)157         protected void setDecorationFillAlpha(int alpha) {
158             if (decorationInfo == null || decorationInfo.getDecorationHandler() == null) {
159                 return;
160             }
161             decorationInfo.getDecorationHandler().setFillAlpha(alpha);
162         }
163     }
164 
165     protected final T mActivityContext;
166     protected final AlphabeticalAppsList<T> mApps;
167     // The text to show when there are no search results and no market search handler.
168     protected int mAppsPerRow;
169 
170     protected final LayoutInflater mLayoutInflater;
171     protected final OnClickListener mOnIconClickListener;
172     protected final OnLongClickListener mOnIconLongClickListener;
173     protected OnFocusChangeListener mIconFocusListener;
174 
BaseAllAppsAdapter(T activityContext, LayoutInflater inflater, AlphabeticalAppsList<T> apps, SearchAdapterProvider<?> adapterProvider)175     public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater,
176             AlphabeticalAppsList<T> apps, SearchAdapterProvider<?> adapterProvider) {
177         mActivityContext = activityContext;
178         mApps = apps;
179         mLayoutInflater = inflater;
180 
181         mOnIconClickListener = mActivityContext.getItemOnClickListener();
182         mOnIconLongClickListener = mActivityContext.getAllAppsItemLongClickListener();
183 
184         mAdapterProvider = adapterProvider;
185     }
186 
187     /** Checks if the passed viewType represents all apps divider. */
isDividerViewType(int viewType)188     public static boolean isDividerViewType(int viewType) {
189         return isViewType(viewType, VIEW_TYPE_MASK_DIVIDER);
190     }
191 
192     /** Checks if the passed viewType represents all apps icon. */
isIconViewType(int viewType)193     public static boolean isIconViewType(int viewType) {
194         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
195     }
196 
197     /** Checks if the passed viewType represents private space header. */
isPrivateSpaceHeaderView(int viewType)198     public static boolean isPrivateSpaceHeaderView(int viewType) {
199         return isViewType(viewType, VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER);
200     }
201 
202     /** Checks if the passed viewType represents private space system apps divider. */
isPrivateSpaceSysAppsDividerView(int viewType)203     public static boolean isPrivateSpaceSysAppsDividerView(int viewType) {
204         return isViewType(viewType, VIEW_TYPE_MASK_PRIVATE_SPACE_SYS_APPS_DIVIDER);
205     }
206 
setIconFocusListener(OnFocusChangeListener focusListener)207     public void setIconFocusListener(OnFocusChangeListener focusListener) {
208         mIconFocusListener = focusListener;
209     }
210 
211     /**
212      * Returns the layout manager.
213      */
getLayoutManager()214     public abstract RecyclerView.LayoutManager getLayoutManager();
215 
216     @Override
onCreateViewHolder(ViewGroup parent, int viewType)217     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
218         switch (viewType) {
219             case VIEW_TYPE_ICON:
220                 int layout = (Flags.enableTwolineToggle()
221                         && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(
222                                 mActivityContext.getApplicationContext()))
223                         ? R.layout.all_apps_icon_twoline : R.layout.all_apps_icon;
224                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
225                         layout, parent, false);
226                 icon.setLongPressTimeoutFactor(1f);
227                 icon.setOnFocusChangeListener(mIconFocusListener);
228                 icon.setOnClickListener(mOnIconClickListener);
229                 icon.setOnLongClickListener(mOnIconLongClickListener);
230                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
231                 icon.getLayoutParams().height =
232                         mActivityContext.getDeviceProfile().allAppsCellHeightPx;
233                 return new ViewHolder(icon);
234             case VIEW_TYPE_EMPTY_SEARCH:
235                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
236                         parent, false));
237             case VIEW_TYPE_ALL_APPS_DIVIDER, VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER:
238                 return new ViewHolder(mLayoutInflater.inflate(
239                         R.layout.private_space_divider, parent, false));
240             case VIEW_TYPE_WORK_EDU_CARD:
241                 return new ViewHolder(mLayoutInflater.inflate(
242                         R.layout.work_apps_edu, parent, false));
243             case VIEW_TYPE_WORK_DISABLED_CARD:
244                 return new ViewHolder(mLayoutInflater.inflate(
245                         R.layout.work_apps_paused, parent, false));
246             case VIEW_TYPE_PRIVATE_SPACE_HEADER:
247                 return new ViewHolder(mLayoutInflater.inflate(
248                         R.layout.private_space_header, parent, false));
249             default:
250                 if (mAdapterProvider.isViewSupported(viewType)) {
251                     return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
252                 }
253                 throw new RuntimeException("Unexpected view type" + viewType);
254         }
255     }
256 
257     @Override
onBindViewHolder(ViewHolder holder, int position)258     public void onBindViewHolder(ViewHolder holder, int position) {
259         holder.itemView.setVisibility(View.VISIBLE);
260         switch (holder.getItemViewType()) {
261             case VIEW_TYPE_ICON: {
262                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
263                 BubbleTextView icon = (BubbleTextView) holder.itemView;
264                 icon.reset();
265                 icon.applyFromApplicationInfo(adapterItem.itemInfo);
266                 icon.setOnFocusChangeListener(mIconFocusListener);
267                 PrivateProfileManager privateProfileManager = mApps.getPrivateProfileManager();
268                 if (privateProfileManager != null) {
269                     // Set the alpha of the private space icon to 0 upon expanding the header so the
270                     // alpha can animate -> 1. This should only be in effect when doing a
271                     // transitioning between Locked/Unlocked state.
272                     boolean isPrivateSpaceItem =
273                             privateProfileManager.isPrivateSpaceItem(adapterItem);
274                     if (icon.getAlpha() == 0 || icon.getAlpha() == 1) {
275                         icon.setAlpha(isPrivateSpaceItem
276                                 && privateProfileManager.isStateTransitioning()
277                                 && (privateProfileManager.isScrolling() ||
278                                     privateProfileManager.getReadyToAnimate())
279                                 && privateProfileManager.getCurrentState() == STATE_ENABLED
280                                 ? 0 : 1);
281                     }
282                     // Views can still be bounded before the app list is updated hence showing icons
283                     // after collapsing.
284                     if (privateProfileManager.getCurrentState() == STATE_DISABLED
285                             && isPrivateSpaceItem) {
286                         adapterItem.decorationInfo = null;
287                         icon.setVisibility(GONE);
288                     }
289                 }
290                 break;
291             }
292             case VIEW_TYPE_EMPTY_SEARCH: {
293                 AppInfo info = mApps.getAdapterItems().get(position).itemInfo;
294                 if (info != null) {
295                     ((TextView) holder.itemView).setText(mActivityContext.getString(
296                             R.string.all_apps_no_search_results, info.title));
297                 }
298                 break;
299             }
300             case VIEW_TYPE_PRIVATE_SPACE_HEADER:
301                 RelativeLayout psHeaderLayout = holder.itemView.findViewById(
302                         R.id.ps_header_layout);
303                 mApps.getPrivateProfileManager().bindPrivateSpaceHeaderViewElements(psHeaderLayout);
304                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
305                 int roundRegions = ROUND_TOP_LEFT | ROUND_TOP_RIGHT;
306                 if (mApps.getPrivateProfileManager().getCurrentState() == STATE_DISABLED) {
307                     roundRegions |= (ROUND_BOTTOM_LEFT | ROUND_BOTTOM_RIGHT);
308                 }
309                 adapterItem.decorationInfo =
310                         new SectionDecorationInfo(mActivityContext, roundRegions,
311                                 false /* decorateTogether */);
312                 break;
313             case VIEW_TYPE_PRIVATE_SPACE_SYS_APPS_DIVIDER:
314                 adapterItem = mApps.getAdapterItems().get(position);
315                 adapterItem.decorationInfo = mApps.getPrivateProfileManager().getCurrentState()
316                         == STATE_DISABLED ? null : new SectionDecorationInfo(mActivityContext,
317                         ROUND_NOTHING, true /* decorateTogether */);
318                 break;
319             case VIEW_TYPE_ALL_APPS_DIVIDER:
320             case VIEW_TYPE_WORK_DISABLED_CARD:
321                 // nothing to do
322                 break;
323             case VIEW_TYPE_WORK_EDU_CARD:
324                 ((WorkEduCard) holder.itemView).setPosition(position);
325                 break;
326             default:
327                 if (mAdapterProvider.isViewSupported(holder.getItemViewType())) {
328                     mAdapterProvider.onBindView(holder, position);
329                 }
330         }
331     }
332 
333     @Override
onFailedToRecycleView(ViewHolder holder)334     public boolean onFailedToRecycleView(ViewHolder holder) {
335         // Always recycle and we will reset the view when it is bound
336         return true;
337     }
338 
339     @Override
getItemCount()340     public int getItemCount() {
341         return mApps.getAdapterItems().size();
342     }
343 
344     @Override
getItemViewType(int position)345     public int getItemViewType(int position) {
346         AdapterItem item = mApps.getAdapterItems().get(position);
347         return item.viewType;
348     }
349 
isViewType(int viewType, int viewTypeMask)350     protected static boolean isViewType(int viewType, int viewTypeMask) {
351         return (viewType & viewTypeMask) != 0;
352     }
353 
354 }
355