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