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