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