1 package com.android.launcher3.popup;
2 
3 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP;
4 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP;
5 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
6 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
7 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
8 
9 import android.app.ActivityOptions;
10 import android.content.ComponentName;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.graphics.Rect;
14 import android.os.Process;
15 import android.os.UserHandle;
16 import android.view.View;
17 import android.view.accessibility.AccessibilityNodeInfo;
18 import android.widget.ImageView;
19 import android.widget.TextView;
20 
21 import androidx.annotation.NonNull;
22 import androidx.annotation.Nullable;
23 
24 import com.android.launcher3.AbstractFloatingView;
25 import com.android.launcher3.AbstractFloatingViewHelper;
26 import com.android.launcher3.Flags;
27 import com.android.launcher3.R;
28 import com.android.launcher3.SecondaryDropTarget;
29 import com.android.launcher3.Utilities;
30 import com.android.launcher3.allapps.PrivateProfileManager;
31 import com.android.launcher3.model.WidgetItem;
32 import com.android.launcher3.model.data.ItemInfo;
33 import com.android.launcher3.model.data.WorkspaceItemInfo;
34 import com.android.launcher3.pm.UserCache;
35 import com.android.launcher3.util.ApiWrapper;
36 import com.android.launcher3.util.ComponentKey;
37 import com.android.launcher3.util.InstantAppResolver;
38 import com.android.launcher3.util.PackageManagerHelper;
39 import com.android.launcher3.util.PackageUserKey;
40 import com.android.launcher3.views.ActivityContext;
41 import com.android.launcher3.widget.WidgetsBottomSheet;
42 
43 import java.util.Arrays;
44 import java.util.List;
45 
46 /**
47  * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
48  * onClickListener that depends on the item that the shortcut services.
49  *
50  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
51  *
52  * @param <T> extends {@link ActivityContext}
53  */
54 public abstract class SystemShortcut<T extends ActivityContext> extends ItemInfo
55         implements View.OnClickListener {
56 
57     private final int mIconResId;
58     protected final int mLabelResId;
59     protected int mAccessibilityActionId;
60 
61     protected final T mTarget;
62     protected final ItemInfo mItemInfo;
63     protected final View mOriginalView;
64 
65     private final AbstractFloatingViewHelper mAbstractFloatingViewHelper;
66 
SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo, View originalView)67     public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo,
68             View originalView) {
69         this(iconResId, labelResId, target, itemInfo, originalView,
70                 new AbstractFloatingViewHelper());
71     }
72 
SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo, View originalView, AbstractFloatingViewHelper abstractFloatingViewHelper)73     public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo,
74             View originalView, AbstractFloatingViewHelper abstractFloatingViewHelper) {
75         mIconResId = iconResId;
76         mLabelResId = labelResId;
77         mAccessibilityActionId = labelResId;
78         mTarget = target;
79         mItemInfo = itemInfo;
80         mOriginalView = originalView;
81         mAbstractFloatingViewHelper = abstractFloatingViewHelper;
82     }
83 
setIconAndLabelFor(View iconView, TextView labelView)84     public void setIconAndLabelFor(View iconView, TextView labelView) {
85         iconView.setBackgroundResource(mIconResId);
86         labelView.setText(mLabelResId);
87     }
88 
setIconAndContentDescriptionFor(ImageView view)89     public void setIconAndContentDescriptionFor(ImageView view) {
90         view.setImageResource(mIconResId);
91         view.setContentDescription(view.getContext().getText(mLabelResId));
92     }
93 
createAccessibilityAction(Context context)94     public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
95         return new AccessibilityNodeInfo.AccessibilityAction(
96                 mAccessibilityActionId, context.getText(mLabelResId));
97     }
98 
hasHandlerForAction(int action)99     public boolean hasHandlerForAction(int action) {
100         return mAccessibilityActionId == action;
101     }
102 
103     public interface Factory<T extends ActivityContext> {
104 
105         @Nullable
getShortcut(T context, ItemInfo itemInfo, @NonNull View originalView)106         SystemShortcut<T> getShortcut(T context, ItemInfo itemInfo, @NonNull View originalView);
107     }
108 
109     public static final Factory<ActivityContext> WIDGETS = (context, itemInfo, originalView) -> {
110         if (itemInfo.getTargetComponent() == null) return null;
111         final List<WidgetItem> widgets =
112                 context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
113                         itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
114         if (widgets.isEmpty()) {
115             return null;
116         }
117         return new Widgets(context, itemInfo, originalView);
118     };
119 
120     public static class Widgets<T extends ActivityContext> extends SystemShortcut<T> {
Widgets(T target, ItemInfo itemInfo, @NonNull View originalView)121         public Widgets(T target, ItemInfo itemInfo, @NonNull View originalView) {
122             super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo,
123                     originalView);
124         }
125 
126         @Override
onClick(View view)127         public void onClick(View view) {
128             AbstractFloatingView.closeAllOpenViews(mTarget);
129             WidgetsBottomSheet widgetsBottomSheet =
130                     (WidgetsBottomSheet) mTarget.getLayoutInflater().inflate(
131                             R.layout.widgets_bottom_sheet, mTarget.getDragLayer(), false);
132             widgetsBottomSheet.populateAndShow(mItemInfo);
133             mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
134                     .log(LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP);
135         }
136     }
137 
138     public static final Factory<ActivityContext> APP_INFO = AppInfo::new;
139 
140     public static class AppInfo<T extends ActivityContext> extends SystemShortcut<T> {
141 
142         @Nullable
143         private SplitAccessibilityInfo mSplitA11yInfo;
144 
AppInfo(T target, ItemInfo itemInfo, @NonNull View originalView)145         public AppInfo(T target, ItemInfo itemInfo, @NonNull View originalView) {
146             super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target,
147                     itemInfo, originalView);
148         }
149 
150         /**
151          * Constructor used by overview for staged split to provide custom A11y information.
152          *
153          * Future improvements considerations:
154          * Have the logic in {@link #createAccessibilityAction(Context)} be moved to super
155          * call in {@link SystemShortcut#createAccessibilityAction(Context)} by having
156          * SystemShortcut be aware of TaskContainers and staged split.
157          * That way it could directly create the correct node info for any shortcut that supports
158          * split, but then we'll need custom resIDs for each pair of shortcuts.
159          */
AppInfo(T target, ItemInfo itemInfo, View originalView, SplitAccessibilityInfo accessibilityInfo)160         public AppInfo(T target, ItemInfo itemInfo, View originalView,
161                 SplitAccessibilityInfo accessibilityInfo) {
162             this(target, itemInfo, originalView);
163             mSplitA11yInfo = accessibilityInfo;
164             mAccessibilityActionId = accessibilityInfo.nodeId;
165         }
166 
167         @Override
createAccessibilityAction( Context context)168         public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(
169                 Context context) {
170             if (mSplitA11yInfo != null && mSplitA11yInfo.containsMultipleTasks) {
171                 String accessibilityLabel = context.getString(R.string.split_app_info_accessibility,
172                         mSplitA11yInfo.taskTitle);
173                 return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId,
174                         accessibilityLabel);
175             } else {
176                 return super.createAccessibilityAction(context);
177             }
178         }
179 
180         @Override
onClick(View view)181         public void onClick(View view) {
182             dismissTaskMenuView();
183             Rect sourceBounds = Utilities.getViewBounds(view);
184             PackageManagerHelper.startDetailsActivityForInfo(view.getContext(), mItemInfo,
185                     sourceBounds, ActivityOptions.makeBasic().toBundle());
186             mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
187                     .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP);
188         }
189 
190         public static class SplitAccessibilityInfo {
191             public final boolean containsMultipleTasks;
192             public final CharSequence taskTitle;
193             public final int nodeId;
194 
SplitAccessibilityInfo(boolean containsMultipleTasks, CharSequence taskTitle, int nodeId)195             public SplitAccessibilityInfo(boolean containsMultipleTasks,
196                     CharSequence taskTitle, int nodeId) {
197                 this.containsMultipleTasks = containsMultipleTasks;
198                 this.taskTitle = taskTitle;
199                 this.nodeId = nodeId;
200             }
201         }
202     }
203 
204     public static final Factory<ActivityContext> PRIVATE_PROFILE_INSTALL =
205             (context, itemInfo, originalView) -> {
206                 if (originalView == null) {
207                     return null;
208                 }
209                 if (itemInfo.getTargetComponent() == null
210                         || !(itemInfo instanceof com.android.launcher3.model.data.AppInfo)
211                         || !itemInfo.getContainerInfo().hasAllAppsContainer()
212                         || !Process.myUserHandle().equals(itemInfo.user)) {
213                     return null;
214                 }
215 
216                 PrivateProfileManager privateProfileManager =
217                         context.getAppsView().getPrivateProfileManager();
218                 if (privateProfileManager == null || !privateProfileManager.isEnabled()) {
219                     return null;
220                 }
221 
222                 UserHandle privateProfileUser = privateProfileManager.getProfileUser();
223                 if (privateProfileUser == null) {
224                     return null;
225                 }
226                 // Do not show shortcut if an app is already installed to the space
227                 ComponentName targetComponent = itemInfo.getTargetComponent();
228                 if (context.getAppsView().getAppsStore().getApp(
229                         new ComponentKey(targetComponent, privateProfileUser)) != null) {
230                     return null;
231                 }
232 
233                 // Do not show shortcut for settings
234                 String[] packagesToSkip =
235                         originalView.getContext().getResources()
236                                 .getStringArray(R.array.skip_private_profile_shortcut_packages);
237                 if (Arrays.asList(packagesToSkip).contains(targetComponent.getPackageName())) {
238                     return null;
239                 }
240 
241                 return new InstallToPrivateProfile<>(
242                         context, itemInfo, originalView, privateProfileUser);
243             };
244 
245     static class InstallToPrivateProfile<T extends ActivityContext> extends SystemShortcut<T> {
246         UserHandle mSpaceUser;
247 
InstallToPrivateProfile(T target, ItemInfo itemInfo, @NonNull View originalView, UserHandle spaceUser)248         InstallToPrivateProfile(T target, ItemInfo itemInfo, @NonNull View originalView,
249                 UserHandle spaceUser) {
250             // TODO(b/302666597): update icon once available
251             super(
252                     R.drawable.ic_install_to_private,
253                     R.string.install_private_system_shortcut_label,
254                     target,
255                     itemInfo,
256                     originalView);
257             mSpaceUser = spaceUser;
258         }
259 
260         @Override
onClick(View view)261         public void onClick(View view) {
262             Intent intent =
263                     ApiWrapper.INSTANCE.get(view.getContext()).getAppMarketActivityIntent(
264                             mItemInfo.getTargetComponent().getPackageName(), mSpaceUser);
265             mTarget.startActivitySafely(view, intent, mItemInfo);
266             AbstractFloatingView.closeAllOpenViews(mTarget);
267             mTarget.getStatsLogManager()
268                     .logger()
269                     .withItemInfo(mItemInfo)
270                     .log(LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP);
271         }
272     }
273 
274     public static final Factory<ActivityContext> INSTALL =
275             (activity, itemInfo, originalView) -> {
276                 if (originalView == null) {
277                     return null;
278                 }
279                 boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo)
280                         && ((WorkspaceItemInfo) itemInfo).hasStatusFlag(
281                         WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI);
282                 boolean isInstantApp = false;
283                 if (itemInfo instanceof com.android.launcher3.model.data.AppInfo) {
284                     com.android.launcher3.model.data.AppInfo
285                             appInfo = (com.android.launcher3.model.data.AppInfo) itemInfo;
286                     isInstantApp = InstantAppResolver.newInstance(
287                             originalView.getContext()).isInstantApp(appInfo);
288                 }
289                 boolean enabled = supportsWebUI || isInstantApp;
290                 if (!enabled) {
291                     return null;
292                 }
293                 return new Install(activity, itemInfo, originalView);
294             };
295 
296     public static class Install<T extends ActivityContext> extends SystemShortcut<T> {
297 
Install(T target, ItemInfo itemInfo, @NonNull View originalView)298         public Install(T target, ItemInfo itemInfo, @NonNull View originalView) {
299             super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label,
300                     target, itemInfo, originalView);
301         }
302 
303         @Override
onClick(View view)304         public void onClick(View view) {
305             Intent intent = ApiWrapper.INSTANCE.get(view.getContext()).getAppMarketActivityIntent(
306                     mItemInfo.getTargetComponent().getPackageName(), Process.myUserHandle());
307             mTarget.startActivitySafely(view, intent, mItemInfo);
308             AbstractFloatingView.closeAllOpenViews(mTarget);
309         }
310     }
311 
312     public static final Factory<ActivityContext> DONT_SUGGEST_APP =
313             (activity, itemInfo, originalView) -> {
314                 if (!itemInfo.isPredictedItem()) {
315                     return null;
316                 }
317                 return new DontSuggestApp<>(activity, itemInfo, originalView);
318             };
319 
320     private static class DontSuggestApp<T extends ActivityContext> extends SystemShortcut<T> {
DontSuggestApp(T target, ItemInfo itemInfo, View originalView)321         DontSuggestApp(T target, ItemInfo itemInfo, View originalView) {
322             super(R.drawable.ic_block_no_shadow, R.string.dismiss_prediction_label, target,
323                     itemInfo, originalView);
324         }
325 
326         @Override
onClick(View view)327         public void onClick(View view) {
328             dismissTaskMenuView();
329             mTarget.getStatsLogManager().logger()
330                     .withItemInfo(mItemInfo)
331                     .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP);
332         }
333     }
334 
335     public static final Factory<ActivityContext> UNINSTALL_APP =
336             (activityContext, itemInfo, originalView) -> {
337                 if (originalView == null) {
338                     return null;
339                 }
340                 if (!Flags.enablePrivateSpace()) {
341                     return null;
342                 }
343                 if (!UserCache.INSTANCE.get(originalView.getContext()).getUserInfo(
344                         itemInfo.user).isPrivate()) {
345                     // If app is not Private Space app.
346                     return null;
347                 }
348                 ComponentName cn = SecondaryDropTarget.getUninstallTarget(originalView.getContext(),
349                         itemInfo);
350                 if (cn == null) {
351                     // If component name is null, don't show uninstall shortcut.
352                     // System apps will have component name as null.
353                     return null;
354                 }
355                 return new UninstallApp(activityContext, itemInfo, originalView, cn);
356             };
357 
358     private static class UninstallApp<T extends ActivityContext> extends SystemShortcut<T> {
359         @NonNull ComponentName mComponentName;
360 
UninstallApp(T target, ItemInfo itemInfo, @NonNull View originalView, @NonNull ComponentName cn)361         UninstallApp(T target, ItemInfo itemInfo, @NonNull View originalView,
362                 @NonNull ComponentName cn) {
363             super(R.drawable.ic_uninstall_no_shadow,
364                     R.string.uninstall_private_system_shortcut_label, target,
365                     itemInfo, originalView);
366             mComponentName = cn;
367 
368         }
369 
370         @Override
onClick(View view)371         public void onClick(View view) {
372             dismissTaskMenuView();
373             SecondaryDropTarget.performUninstall(view.getContext(), mComponentName, mItemInfo);
374             mTarget.getStatsLogManager()
375                     .logger()
376                     .withItemInfo(mItemInfo)
377                     .log(LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP);
378         }
379     }
380 
dismissTaskMenuView()381     protected void dismissTaskMenuView() {
382         mAbstractFloatingViewHelper.closeOpenViews(mTarget, true,
383                 AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
384     }
385 }
386