package com.android.launcher3.popup; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_TASK; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.graphics.Rect; import android.net.Uri; import android.os.Process; import android.os.UserHandle; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import android.os.UserHandle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.SecondaryDropTarget; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.PrivateProfileManager; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.uioverrides.ApiWrapper; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.WidgetsBottomSheet; import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; /** * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an * onClickListener that depends on the item that the shortcut services. * * Example system shortcuts, defined as inner classes, include Widgets and AppInfo. * * @param extends {@link ActivityContext} */ public abstract class SystemShortcut extends ItemInfo implements View.OnClickListener { private final int mIconResId; protected final int mLabelResId; protected int mAccessibilityActionId; protected final T mTarget; protected final ItemInfo mItemInfo; protected final View mOriginalView; public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo, View originalView) { mIconResId = iconResId; mLabelResId = labelResId; mAccessibilityActionId = labelResId; mTarget = target; mItemInfo = itemInfo; mOriginalView = originalView; } public SystemShortcut(SystemShortcut other) { mIconResId = other.mIconResId; mLabelResId = other.mLabelResId; mAccessibilityActionId = other.mAccessibilityActionId; mTarget = other.mTarget; mItemInfo = other.mItemInfo; mOriginalView = other.mOriginalView; } public void setIconAndLabelFor(View iconView, TextView labelView) { iconView.setBackgroundResource(mIconResId); labelView.setText(mLabelResId); } public void setIconAndContentDescriptionFor(ImageView view) { view.setImageResource(mIconResId); view.setContentDescription(view.getContext().getText(mLabelResId)); } public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) { return new AccessibilityNodeInfo.AccessibilityAction( mAccessibilityActionId, context.getText(mLabelResId)); } public boolean hasHandlerForAction(int action) { return mAccessibilityActionId == action; } public interface Factory { @Nullable SystemShortcut getShortcut(T context, ItemInfo itemInfo, @NonNull View originalView); } public static final Factory WIDGETS = (context, itemInfo, originalView) -> { if (itemInfo.getTargetComponent() == null) return null; final List widgets = context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey( itemInfo.getTargetComponent().getPackageName(), itemInfo.user)); if (widgets.isEmpty()) { return null; } return new Widgets(context, itemInfo, originalView); }; public static class Widgets extends SystemShortcut { public Widgets(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo, originalView); } @Override public void onClick(View view) { AbstractFloatingView.closeAllOpenViews(mTarget); WidgetsBottomSheet widgetsBottomSheet = (WidgetsBottomSheet) mTarget.getLayoutInflater().inflate( R.layout.widgets_bottom_sheet, mTarget.getDragLayer(), false); widgetsBottomSheet.populateAndShow(mItemInfo); mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP); } } public static final Factory APP_INFO = AppInfo::new; public static class AppInfo extends SystemShortcut { @Nullable private SplitAccessibilityInfo mSplitA11yInfo; public AppInfo(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target, itemInfo, originalView); } /** * Constructor used by overview for staged split to provide custom A11y information. * * Future improvements considerations: * Have the logic in {@link #createAccessibilityAction(Context)} be moved to super * call in {@link SystemShortcut#createAccessibilityAction(Context)} by having * SystemShortcut be aware of TaskContainers and staged split. * That way it could directly create the correct node info for any shortcut that supports * split, but then we'll need custom resIDs for each pair of shortcuts. */ public AppInfo(T target, ItemInfo itemInfo, View originalView, SplitAccessibilityInfo accessibilityInfo) { this(target, itemInfo, originalView); mSplitA11yInfo = accessibilityInfo; mAccessibilityActionId = accessibilityInfo.nodeId; } @Override public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction( Context context) { if (mSplitA11yInfo != null && mSplitA11yInfo.containsMultipleTasks) { String accessibilityLabel = context.getString(R.string.split_app_info_accessibility, mSplitA11yInfo.taskTitle); return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId, accessibilityLabel); } else { return super.createAccessibilityAction(context); } } @Override public void onClick(View view) { dismissTaskMenuView(mTarget); Rect sourceBounds = Utilities.getViewBounds(view); new PackageManagerHelper(view.getContext()).startDetailsActivityForInfo( mItemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle()); mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP); } public static class SplitAccessibilityInfo { public final boolean containsMultipleTasks; public final CharSequence taskTitle; public final int nodeId; public SplitAccessibilityInfo(boolean containsMultipleTasks, CharSequence taskTitle, int nodeId) { this.containsMultipleTasks = containsMultipleTasks; this.taskTitle = taskTitle; this.nodeId = nodeId; } } } public static final Factory PRIVATE_PROFILE_INSTALL = (context, itemInfo, originalView) -> { if (originalView == null) { return null; } if (itemInfo.getTargetComponent() == null || !(itemInfo instanceof com.android.launcher3.model.data.AppInfo) || !itemInfo.getContainerInfo().hasAllAppsContainer() || !Process.myUserHandle().equals(itemInfo.user)) { return null; } PrivateProfileManager privateProfileManager = context.getAppsView().getPrivateProfileManager(); if (privateProfileManager == null || !privateProfileManager.isEnabled()) { return null; } UserHandle privateProfileUser = privateProfileManager.getProfileUser(); if (privateProfileUser == null) { return null; } // Do not show shortcut if an app is already installed to the space ComponentName targetComponent = itemInfo.getTargetComponent(); if (context.getAppsView().getAppsStore().getApp( new ComponentKey(targetComponent, privateProfileUser)) != null) { return null; } // Do not show shortcut for settings String[] packagesToSkip = originalView.getContext().getResources() .getStringArray(R.array.skip_private_profile_shortcut_packages); if (Arrays.asList(packagesToSkip).contains(targetComponent.getPackageName())) { return null; } return new InstallToPrivateProfile<>( context, itemInfo, originalView, privateProfileUser); }; static class InstallToPrivateProfile extends SystemShortcut { UserHandle mSpaceUser; InstallToPrivateProfile(T target, ItemInfo itemInfo, @NonNull View originalView, UserHandle spaceUser) { // TODO(b/302666597): update icon once available super( R.drawable.ic_install_to_private, R.string.install_private_system_shortcut_label, target, itemInfo, originalView); mSpaceUser = spaceUser; } @Override public void onClick(View view) { Intent intent = ApiWrapper.getAppMarketActivityIntent( view.getContext(), mItemInfo.getTargetComponent().getPackageName(), mSpaceUser); mTarget.startActivitySafely(view, intent, mItemInfo); AbstractFloatingView.closeAllOpenViews(mTarget); mTarget.getStatsLogManager() .logger() .withItemInfo(mItemInfo) .log(LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP); } } public static final Factory INSTALL = (activity, itemInfo, originalView) -> { if (originalView == null) { return null; } boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) && ((WorkspaceItemInfo) itemInfo).hasStatusFlag( WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI); boolean isInstantApp = false; if (itemInfo instanceof com.android.launcher3.model.data.AppInfo) { com.android.launcher3.model.data.AppInfo appInfo = (com.android.launcher3.model.data.AppInfo) itemInfo; isInstantApp = InstantAppResolver.newInstance( originalView.getContext()).isInstantApp(appInfo); } boolean enabled = supportsWebUI || isInstantApp; if (!enabled) { return null; } return new Install(activity, itemInfo, originalView); }; public static class Install extends SystemShortcut { public Install(T target, ItemInfo itemInfo, @NonNull View originalView) { super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label, target, itemInfo, originalView); } @Override public void onClick(View view) { Intent intent = ApiWrapper.getAppMarketActivityIntent(view.getContext(), mItemInfo.getTargetComponent().getPackageName(), Process.myUserHandle()); mTarget.startActivitySafely(view, intent, mItemInfo); AbstractFloatingView.closeAllOpenViews(mTarget); } } public static final Factory DONT_SUGGEST_APP = (activity, itemInfo, originalView) -> { if (!itemInfo.isPredictedItem()) { return null; } return new DontSuggestApp<>(activity, itemInfo, originalView); }; private static class DontSuggestApp extends SystemShortcut { DontSuggestApp(T target, ItemInfo itemInfo, View originalView) { super(R.drawable.ic_block_no_shadow, R.string.dismiss_prediction_label, target, itemInfo, originalView); } @Override public void onClick(View view) { dismissTaskMenuView(mTarget); mTarget.getStatsLogManager().logger() .withItemInfo(mItemInfo) .log(LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP); } } public static final Factory UNINSTALL_APP = (activityContext, itemInfo, originalView) -> { if (originalView == null) { return null; } if (!Flags.enablePrivateSpace()) { return null; } if (!UserCache.INSTANCE.get(originalView.getContext()).getUserInfo( itemInfo.user).isPrivate()) { // If app is not Private Space app. return null; } ComponentName cn = SecondaryDropTarget.getUninstallTarget(originalView.getContext(), itemInfo); if (cn == null) { // If component name is null, don't show uninstall shortcut. // System apps will have component name as null. return null; } return new UninstallApp(activityContext, itemInfo, originalView, cn); }; private static class UninstallApp extends SystemShortcut { @NonNull ComponentName mComponentName; UninstallApp(T target, ItemInfo itemInfo, @NonNull View originalView, @NonNull ComponentName cn) { super(R.drawable.ic_uninstall_no_shadow, R.string.uninstall_drop_target_label, target, itemInfo, originalView); mComponentName = cn; } @Override public void onClick(View view) { dismissTaskMenuView(mTarget); SecondaryDropTarget.performUninstall(view.getContext(), mComponentName, mItemInfo); mTarget.getStatsLogManager() .logger() .withItemInfo(mItemInfo) .log(LAUNCHER_PRIVATE_SPACE_UNINSTALL_SYSTEM_SHORTCUT_TAP); } } public static final Factory UNINSTALL = (activity, itemInfo, originalView) -> itemInfo.getTargetComponent() == null || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT || itemInfo.itemType == ITEM_TYPE_SHORTCUT || PackageManagerHelper.isSystemApp((Context) activity, itemInfo.getTargetComponent().getPackageName()) ? null : new UnInstall(activity, itemInfo, originalView); public static class UnInstall extends SystemShortcut { public UnInstall(T target, ItemInfo itemInfo, View originalView) { super(R.drawable.ic_uninstall_no_shadow, R.string.uninstall_drop_target_label, target, itemInfo, originalView); } /** * @return the component name that should be uninstalled or null. */ private ComponentName getUninstallTarget(ItemInfo item, Context context) { Intent intent = null; UserHandle user = null; if (item != null && (item.itemType == ITEM_TYPE_APPLICATION || item.itemType == ITEM_TYPE_TASK)) { intent = item.getIntent(); user = item.user; } if (intent != null) { LauncherActivityInfo info = context.getSystemService(LauncherApps.class) .resolveActivity(intent, user); if (info != null && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) { return info.getComponentName(); } } return null; } @Override public void onClick(View view) { ComponentName cn = getUninstallTarget(mItemInfo, view.getContext()); if (cn == null) { // System applications cannot be installed. For now, show a toast explaining that. // We may give them the option of disabling apps this way. Toast.makeText(view.getContext(), R.string.uninstall_system_app_text, Toast.LENGTH_SHORT).show(); return; } try { Intent intent = Intent.parseUri(view.getContext().getString(R.string.delete_package_intent), 0) .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName())) .putExtra(Intent.EXTRA_USER, mItemInfo.user); ((Context) mTarget).startActivity(intent); AbstractFloatingView.closeAllOpenViews(mTarget); } catch (URISyntaxException e) { // Do nothing. } } } public static void dismissTaskMenuView(T activity) { AbstractFloatingView.closeOpenViews(activity, true, AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); } }