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