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