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