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