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