1 package com.android.launcher3; 2 3 import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID; 4 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE; 5 6 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION; 7 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.INVALID; 8 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE; 9 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL; 10 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DISMISS_PREDICTION_UNDO; 11 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST; 12 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL; 13 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED; 14 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_COMPLETED; 15 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_MASK; 16 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_NO; 17 18 import android.appwidget.AppWidgetHostView; 19 import android.appwidget.AppWidgetProviderInfo; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.LauncherActivityInfo; 25 import android.content.pm.LauncherApps; 26 import android.content.pm.PackageManager; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.UserHandle; 30 import android.os.UserManager; 31 import android.util.ArrayMap; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.View; 35 import android.widget.Toast; 36 37 import androidx.annotation.Nullable; 38 39 import com.android.launcher3.config.FeatureFlags; 40 import com.android.launcher3.dragndrop.DragOptions; 41 import com.android.launcher3.logging.FileLog; 42 import com.android.launcher3.logging.InstanceId; 43 import com.android.launcher3.logging.InstanceIdSequence; 44 import com.android.launcher3.logging.StatsLogManager; 45 import com.android.launcher3.logging.StatsLogManager.StatsLogger; 46 import com.android.launcher3.model.data.ItemInfo; 47 import com.android.launcher3.model.data.ItemInfoWithIcon; 48 import com.android.launcher3.pm.UserCache; 49 import com.android.launcher3.util.PackageManagerHelper; 50 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 51 52 import java.net.URISyntaxException; 53 54 /** 55 * Drop target which provides a secondary option for an item. 56 * For app targets: shows as uninstall 57 * For configurable widgets: shows as setup 58 * For predicted app icons: don't suggest app 59 */ 60 public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmListener { 61 62 private static final String TAG = "SecondaryDropTarget"; 63 64 private static final long CACHE_EXPIRE_TIMEOUT = 5000; 65 private final ArrayMap<UserHandle, Boolean> mUninstallDisabledCache = new ArrayMap<>(1); 66 private final StatsLogManager mStatsLogManager; 67 private final Alarm mCacheExpireAlarm; 68 private boolean mHadPendingAlarm; 69 70 protected int mCurrentAccessibilityAction = -1; 71 SecondaryDropTarget(Context context, AttributeSet attrs)72 public SecondaryDropTarget(Context context, AttributeSet attrs) { 73 this(context, attrs, 0); 74 } 75 SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle)76 public SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle) { 77 super(context, attrs, defStyle); 78 mCacheExpireAlarm = new Alarm(); 79 mStatsLogManager = StatsLogManager.newInstance(context); 80 } 81 82 @Override onAttachedToWindow()83 protected void onAttachedToWindow() { 84 super.onAttachedToWindow(); 85 if (mHadPendingAlarm) { 86 mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT); 87 mCacheExpireAlarm.setOnAlarmListener(this); 88 mHadPendingAlarm = false; 89 } 90 } 91 92 @Override onDetachedFromWindow()93 protected void onDetachedFromWindow() { 94 super.onDetachedFromWindow(); 95 if (mCacheExpireAlarm.alarmPending()) { 96 mCacheExpireAlarm.cancelAlarm(); 97 mCacheExpireAlarm.setOnAlarmListener(null); 98 mHadPendingAlarm = true; 99 } 100 } 101 102 @Override onFinishInflate()103 protected void onFinishInflate() { 104 super.onFinishInflate(); 105 setupUi(UNINSTALL); 106 } 107 setupUi(int action)108 protected void setupUi(int action) { 109 if (action == mCurrentAccessibilityAction) { 110 return; 111 } 112 mCurrentAccessibilityAction = action; 113 114 if (action == UNINSTALL) { 115 setDrawable(R.drawable.ic_uninstall_no_shadow); 116 updateText(R.string.uninstall_drop_target_label); 117 } else if (action == DISMISS_PREDICTION) { 118 setDrawable(R.drawable.ic_block_no_shadow); 119 updateText(R.string.dismiss_prediction_label); 120 } else if (action == RECONFIGURE) { 121 setDrawable(R.drawable.ic_setting); 122 updateText(R.string.gadget_setup_text); 123 } 124 } 125 126 @Override onAlarm(Alarm alarm)127 public void onAlarm(Alarm alarm) { 128 mUninstallDisabledCache.clear(); 129 } 130 131 @Override getAccessibilityAction()132 public int getAccessibilityAction() { 133 return mCurrentAccessibilityAction; 134 } 135 136 @Override setupItemInfo(ItemInfo info)137 protected void setupItemInfo(ItemInfo info) { 138 int buttonType = getButtonType(info, getViewUnderDrag(info)); 139 if (buttonType != INVALID) { 140 setupUi(buttonType); 141 } 142 } 143 144 @Override supportsDrop(ItemInfo info)145 protected boolean supportsDrop(ItemInfo info) { 146 return getButtonType(info, getViewUnderDrag(info)) != INVALID; 147 } 148 149 @Override supportsAccessibilityDrop(ItemInfo info, View view)150 public boolean supportsAccessibilityDrop(ItemInfo info, View view) { 151 return getButtonType(info, view) != INVALID; 152 } 153 getButtonType(ItemInfo info, View view)154 private int getButtonType(ItemInfo info, View view) { 155 if (view instanceof AppWidgetHostView) { 156 if (getReconfigurableWidgetId(view) != INVALID_APPWIDGET_ID) { 157 return RECONFIGURE; 158 } 159 return INVALID; 160 } else if (info.isPredictedItem()) { 161 if (Flags.enableShortcutDontSuggestApp()) { 162 return INVALID; 163 } 164 return DISMISS_PREDICTION; 165 } 166 167 Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user); 168 if (uninstallDisabled == null) { 169 UserManager userManager = 170 (UserManager) getContext().getSystemService(Context.USER_SERVICE); 171 Bundle restrictions = userManager.getUserRestrictions(info.user); 172 uninstallDisabled = restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) 173 || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false); 174 mUninstallDisabledCache.put(info.user, uninstallDisabled); 175 } 176 // Cancel any pending alarm and set cache expiry after some time 177 mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT); 178 mCacheExpireAlarm.setOnAlarmListener(this); 179 if (uninstallDisabled) { 180 return INVALID; 181 } 182 if (Flags.enablePrivateSpace() && UserCache.getInstance(getContext()).getUserInfo( 183 info.user).isPrivate()) { 184 return INVALID; 185 } 186 187 if (info instanceof ItemInfoWithIcon) { 188 ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info; 189 if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0 190 && (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) == 0) { 191 return INVALID; 192 } 193 } 194 if (getUninstallTarget(getContext(), info) == null) { 195 return INVALID; 196 } 197 return UNINSTALL; 198 } 199 200 /** 201 * @return the component name that should be uninstalled or null. 202 */ getUninstallTarget(Context context, ItemInfo item)203 public static ComponentName getUninstallTarget(Context context, ItemInfo item) { 204 Intent intent = null; 205 UserHandle user = null; 206 if (item != null && 207 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 208 intent = item.getIntent(); 209 user = item.user; 210 } 211 if (intent != null) { 212 LauncherActivityInfo info = context.getSystemService(LauncherApps.class) 213 .resolveActivity(intent, user); 214 if (info != null 215 && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 216 return info.getComponentName(); 217 } 218 } 219 return null; 220 } 221 222 @Override onDrop(DragObject d, DragOptions options)223 public void onDrop(DragObject d, DragOptions options) { 224 // Defer onComplete 225 d.dragSource = new DeferredOnComplete(d.dragSource, getContext()); 226 227 super.onDrop(d, options); 228 doLog(d.logInstanceId, d.originalDragInfo); 229 } 230 doLog(InstanceId logInstanceId, ItemInfo itemInfo)231 private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) { 232 StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId); 233 if (itemInfo != null) { 234 logger.withItemInfo(itemInfo); 235 } 236 if (mCurrentAccessibilityAction == UNINSTALL) { 237 logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL); 238 } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { 239 logger.log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST); 240 } 241 } 242 243 @Override completeDrop(final DragObject d)244 public void completeDrop(final DragObject d) { 245 ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo, 246 d.logInstanceId); 247 mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d); 248 } 249 getViewUnderDrag(ItemInfo info)250 private View getViewUnderDrag(ItemInfo info) { 251 return mDropTargetHandler.getViewUnderDrag(info); 252 } 253 254 /** 255 * Verifies that the view is an reconfigurable widget and returns the corresponding widget Id, 256 * otherwise return {@code INVALID_APPWIDGET_ID} 257 */ getReconfigurableWidgetId(View view)258 private int getReconfigurableWidgetId(View view) { 259 if (!(view instanceof AppWidgetHostView)) { 260 return INVALID_APPWIDGET_ID; 261 } 262 AppWidgetHostView hostView = (AppWidgetHostView) view; 263 AppWidgetProviderInfo widgetInfo = hostView.getAppWidgetInfo(); 264 if (widgetInfo == null || widgetInfo.configure == null) { 265 return INVALID_APPWIDGET_ID; 266 } 267 if ( (LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), widgetInfo) 268 .getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) == 0) { 269 return INVALID_APPWIDGET_ID; 270 } 271 return hostView.getAppWidgetId(); 272 } 273 274 /** 275 * Performs the drop action and returns the target component for the dragObject or null if 276 * the action was not performed. 277 */ performDropAction(View view, ItemInfo info, InstanceId instanceId)278 protected ComponentName performDropAction(View view, ItemInfo info, InstanceId instanceId) { 279 if (mCurrentAccessibilityAction == RECONFIGURE) { 280 int widgetId = getReconfigurableWidgetId(view); 281 if (widgetId != INVALID_APPWIDGET_ID) { 282 mDropTargetHandler.reconfigureWidget(widgetId, info); 283 } 284 return null; 285 } 286 if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { 287 if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) { 288 CharSequence announcement = getContext().getString(R.string.item_removed); 289 mDropTargetHandler 290 .dismissPrediction(announcement, () -> { 291 }, () -> { 292 mStatsLogManager.logger() 293 .withInstanceId(instanceId) 294 .withItemInfo(info) 295 .log(LAUNCHER_DISMISS_PREDICTION_UNDO); 296 }); 297 } 298 return null; 299 } 300 301 return performUninstall(getContext(), getUninstallTarget(getContext(), info), info); 302 } 303 304 /** 305 * Performs uninstall and returns the target component for the {@link ItemInfo} or null if 306 * the uninstall was not performed. 307 */ performUninstall(Context context, @Nullable ComponentName cn, ItemInfo info)308 public static ComponentName performUninstall(Context context, @Nullable ComponentName cn, 309 ItemInfo info) { 310 if (cn == null) { 311 // System applications cannot be installed. For now, show a toast explaining that. 312 // We may give them the option of disabling apps this way. 313 Toast.makeText( 314 context, 315 R.string.uninstall_system_app_text, 316 Toast.LENGTH_SHORT 317 ).show(); 318 return null; 319 } 320 try { 321 Intent i = Intent.parseUri(context.getString(R.string.delete_package_intent), 0) 322 .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName())) 323 .putExtra(Intent.EXTRA_USER, info.user); 324 context.startActivity(i); 325 FileLog.d(TAG, "start uninstall activity " + cn.getPackageName()); 326 return cn; 327 } catch (URISyntaxException e) { 328 Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info); 329 return null; 330 } 331 } 332 333 @Override onAccessibilityDrop(View view, ItemInfo item)334 public void onAccessibilityDrop(View view, ItemInfo item) { 335 InstanceId instanceId = new InstanceIdSequence().newInstanceId(); 336 doLog(instanceId, item); 337 performDropAction(view, item, instanceId); 338 } 339 340 /** 341 * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until 342 * {@link #onLauncherResume} 343 */ 344 protected class DeferredOnComplete implements DragSource { 345 346 private final DragSource mOriginal; 347 private final Context mContext; 348 349 protected String mPackageName; 350 private DragObject mDragObject; 351 DeferredOnComplete(DragSource original, Context context)352 public DeferredOnComplete(DragSource original, Context context) { 353 mOriginal = original; 354 mContext = context; 355 } 356 357 @Override onDropCompleted(View target, DragObject d, boolean success)358 public void onDropCompleted(View target, DragObject d, 359 boolean success) { 360 mDragObject = d; 361 } 362 onLauncherResume()363 public void onLauncherResume() { 364 // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well. 365 if (PackageManagerHelper.INSTANCE.get(mContext).getApplicationInfo(mPackageName, 366 mDragObject.dragInfo.user, PackageManager.MATCH_UNINSTALLED_PACKAGES) == null) { 367 mDragObject.dragSource = mOriginal; 368 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true); 369 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId) 370 .log(LAUNCHER_ITEM_UNINSTALL_COMPLETED); 371 } else { 372 sendFailure(); 373 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId) 374 .log(LAUNCHER_ITEM_UNINSTALL_CANCELLED); 375 } 376 } 377 sendFailure()378 public void sendFailure() { 379 mDragObject.dragSource = mOriginal; 380 mDragObject.cancelled = true; 381 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false); 382 } 383 } 384 } 385