1package com.android.launcher3; 2 3import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID; 4import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE; 5 6import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK; 7import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO; 8import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; 9import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE; 10import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL; 11 12import android.appwidget.AppWidgetHostView; 13import android.appwidget.AppWidgetManager; 14import android.appwidget.AppWidgetProviderInfo; 15import android.content.ComponentName; 16import android.content.Context; 17import android.content.Intent; 18import android.content.pm.ApplicationInfo; 19import android.content.pm.LauncherActivityInfo; 20import android.content.pm.PackageManager; 21import android.net.Uri; 22import android.os.Bundle; 23import android.os.UserHandle; 24import android.os.UserManager; 25import android.util.ArrayMap; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.view.View; 29import android.widget.Toast; 30 31import com.android.launcher3.Launcher.OnResumeCallback; 32import com.android.launcher3.compat.LauncherAppsCompat; 33import com.android.launcher3.dragndrop.DragOptions; 34import com.android.launcher3.logging.LoggerUtils; 35import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; 36import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 37import com.android.launcher3.util.Themes; 38 39import 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 */ 46public 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; 56 public SecondaryDropTarget(Context context, AttributeSet attrs) { 57 this(context, attrs, 0); 58 } 59 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 68 protected void onFinishInflate() { 69 super.onFinishInflate(); 70 setupUi(UNINSTALL); 71 } 72 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 91 public void onAlarm(Alarm alarm) { 92 mUninstallDisabledCache.clear(); 93 } 94 95 @Override 96 public int getAccessibilityAction() { 97 return mCurrentAccessibilityAction; 98 } 99 100 @Override 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 109 protected boolean supportsDrop(ItemInfo info) { 110 return supportsAccessibilityDrop(info, getViewUnderDrag(info)); 111 } 112 113 @Override 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 */ 151 private ComponentName getUninstallTarget(ItemInfo item) { 152 Intent intent = null; 153 UserHandle user = null; 154 if (item != null && 155 item.itemType == LauncherSettings.BaseLauncherColumns.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 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 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.setOnResumeCallback(deferred); 185 } else { 186 deferred.sendFailure(); 187 } 188 } 189 } 190 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 */ 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 */ 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 return cn; 246 } catch (URISyntaxException e) { 247 Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info); 248 return null; 249 } 250 } 251 252 @Override 253 public void onAccessibilityDrop(View view, ItemInfo item) { 254 performDropAction(view, item); 255 } 256 257 /** 258 * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until 259 * {@link #onLauncherResume} 260 */ 261 private class DeferredOnComplete implements DragSource, OnResumeCallback { 262 263 private final DragSource mOriginal; 264 private final Context mContext; 265 266 private String mPackageName; 267 private DragObject mDragObject; 268 269 public DeferredOnComplete(DragSource original, Context context) { 270 mOriginal = original; 271 mContext = context; 272 } 273 274 @Override 275 public void onDropCompleted(View target, DragObject d, 276 boolean success) { 277 mDragObject = d; 278 } 279 280 @Override 281 public void fillInLogContainerData(View v, ItemInfo info, Target target, 282 Target targetParent) { 283 mOriginal.fillInLogContainerData(v, info, target, targetParent); 284 } 285 286 @Override 287 public void onLauncherResume() { 288 // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well. 289 if (LauncherAppsCompat.getInstance(mContext) 290 .getApplicationInfo(mPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, 291 mDragObject.dragInfo.user) == null) { 292 mDragObject.dragSource = mOriginal; 293 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true); 294 } else { 295 sendFailure(); 296 } 297 } 298 299 public void sendFailure() { 300 mDragObject.dragSource = mOriginal; 301 mDragObject.cancelled = true; 302 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false); 303 } 304 } 305} 306