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