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_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.net.Uri; 26 import android.os.Bundle; 27 import android.os.UserHandle; 28 import android.os.UserManager; 29 import android.util.ArrayMap; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.view.View; 33 import android.widget.Toast; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.launcher3.dragndrop.DragOptions; 38 import com.android.launcher3.logging.FileLog; 39 import com.android.launcher3.logging.InstanceId; 40 import com.android.launcher3.logging.InstanceIdSequence; 41 import com.android.launcher3.logging.StatsLogManager; 42 import com.android.launcher3.logging.StatsLogManager.StatsLogger; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.model.data.ItemInfoWithIcon; 45 import com.android.launcher3.pm.UserCache; 46 import com.android.launcher3.util.ApplicationInfoWrapper; 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 if (Flags.enableShortcutDontSuggestApp()) { 159 return INVALID; 160 } 161 return DISMISS_PREDICTION; 162 } 163 164 Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user); 165 if (uninstallDisabled == null) { 166 UserManager userManager = 167 (UserManager) getContext().getSystemService(Context.USER_SERVICE); 168 Bundle restrictions = userManager.getUserRestrictions(info.user); 169 uninstallDisabled = restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false) 170 || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false); 171 mUninstallDisabledCache.put(info.user, uninstallDisabled); 172 } 173 // Cancel any pending alarm and set cache expiry after some time 174 mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT); 175 mCacheExpireAlarm.setOnAlarmListener(this); 176 if (uninstallDisabled) { 177 return INVALID; 178 } 179 if (Flags.enablePrivateSpace() && UserCache.getInstance(getContext()).getUserInfo( 180 info.user).isPrivate()) { 181 return INVALID; 182 } 183 184 if (info instanceof ItemInfoWithIcon) { 185 ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info; 186 if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0 187 && (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) == 0) { 188 return INVALID; 189 } 190 } 191 if (getUninstallTarget(getContext(), info) == null) { 192 return INVALID; 193 } 194 return UNINSTALL; 195 } 196 197 /** 198 * @return the component name that should be uninstalled or null. 199 */ getUninstallTarget(Context context, ItemInfo item)200 public static ComponentName getUninstallTarget(Context context, ItemInfo item) { 201 Intent intent = null; 202 UserHandle user = null; 203 if (item != null && 204 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 205 intent = item.getIntent(); 206 user = item.user; 207 } 208 if (intent != null) { 209 LauncherActivityInfo info = context.getSystemService(LauncherApps.class) 210 .resolveActivity(intent, user); 211 if (info != null 212 && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 213 return info.getComponentName(); 214 } 215 } 216 return null; 217 } 218 219 @Override onDrop(DragObject d, DragOptions options)220 public void onDrop(DragObject d, DragOptions options) { 221 // Defer onComplete 222 d.dragSource = new DeferredOnComplete(d.dragSource, getContext()); 223 224 super.onDrop(d, options); 225 doLog(d.logInstanceId, d.originalDragInfo); 226 } 227 doLog(InstanceId logInstanceId, ItemInfo itemInfo)228 private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) { 229 StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId); 230 if (itemInfo != null) { 231 logger.withItemInfo(itemInfo); 232 } 233 if (mCurrentAccessibilityAction == UNINSTALL) { 234 logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL); 235 } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { 236 logger.log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST); 237 } 238 } 239 240 @Override completeDrop(final DragObject d)241 public void completeDrop(final DragObject d) { 242 ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo); 243 mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d); 244 } 245 getViewUnderDrag(ItemInfo info)246 private View getViewUnderDrag(ItemInfo info) { 247 return mDropTargetHandler.getViewUnderDrag(info); 248 } 249 250 /** 251 * Verifies that the view is an reconfigurable widget and returns the corresponding widget Id, 252 * otherwise return {@code INVALID_APPWIDGET_ID} 253 */ getReconfigurableWidgetId(View view)254 private int getReconfigurableWidgetId(View view) { 255 if (!(view instanceof AppWidgetHostView)) { 256 return INVALID_APPWIDGET_ID; 257 } 258 AppWidgetHostView hostView = (AppWidgetHostView) view; 259 AppWidgetProviderInfo widgetInfo = hostView.getAppWidgetInfo(); 260 if (widgetInfo == null || widgetInfo.configure == null) { 261 return INVALID_APPWIDGET_ID; 262 } 263 if ( (LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), widgetInfo) 264 .getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) == 0) { 265 return INVALID_APPWIDGET_ID; 266 } 267 return hostView.getAppWidgetId(); 268 } 269 270 /** 271 * Performs the drop action and returns the target component for the dragObject or null if 272 * the action was not performed. 273 */ performDropAction(View view, ItemInfo info)274 protected ComponentName performDropAction(View view, ItemInfo info) { 275 if (mCurrentAccessibilityAction == RECONFIGURE) { 276 int widgetId = getReconfigurableWidgetId(view); 277 if (widgetId != INVALID_APPWIDGET_ID) { 278 mDropTargetHandler.reconfigureWidget(widgetId, info); 279 } 280 return null; 281 } 282 return performUninstall(getContext(), getUninstallTarget(getContext(), info), info); 283 } 284 285 /** 286 * Performs uninstall and returns the target component for the {@link ItemInfo} or null if 287 * the uninstall was not performed. 288 */ performUninstall(Context context, @Nullable ComponentName cn, ItemInfo info)289 public static ComponentName performUninstall(Context context, @Nullable ComponentName cn, 290 ItemInfo 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 context, 296 R.string.uninstall_system_app_text, 297 Toast.LENGTH_SHORT 298 ).show(); 299 return null; 300 } 301 try { 302 Intent i = Intent.parseUri(context.getString(R.string.delete_package_intent), 0) 303 .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName())) 304 .putExtra(Intent.EXTRA_USER, info.user); 305 context.startActivity(i); 306 FileLog.d(TAG, "start uninstall activity from drop target " + cn.getPackageName()); 307 return cn; 308 } catch (URISyntaxException e) { 309 Log.e(TAG, "Failed to parse intent to start drop target uninstall activity for" 310 + " item=" + info); 311 return null; 312 } 313 } 314 315 @Override onAccessibilityDrop(View view, ItemInfo item)316 public void onAccessibilityDrop(View view, ItemInfo item) { 317 doLog(new InstanceIdSequence().newInstanceId(), item); 318 performDropAction(view, item); 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 if (new ApplicationInfoWrapper(mContext, mPackageName, mDragObject.dragInfo.user) 346 .getInfo() == null) { 347 mDragObject.dragSource = mOriginal; 348 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true); 349 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId) 350 .log(LAUNCHER_ITEM_UNINSTALL_COMPLETED); 351 } else { 352 sendFailure(); 353 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId) 354 .log(LAUNCHER_ITEM_UNINSTALL_CANCELLED); 355 } 356 } 357 sendFailure()358 public void sendFailure() { 359 mDragObject.dragSource = mOriginal; 360 mDragObject.cancelled = true; 361 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false); 362 } 363 } 364 } 365