• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.LauncherSettings.Favorites.CONTAINER_DESKTOP;
7 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION;
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_ITEM_DROPPED_ON_DONT_SUGGEST;
11 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_UNINSTALL;
12 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_CANCELLED;
13 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_UNINSTALL_COMPLETED;
14 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
15 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SYSTEM_NO;
16 
17 import android.appwidget.AppWidgetHostView;
18 import android.appwidget.AppWidgetProviderInfo;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.LauncherActivityInfo;
24 import android.content.pm.LauncherApps;
25 import android.content.pm.PackageManager;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.util.ArrayMap;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.view.View;
34 import android.widget.Toast;
35 
36 import com.android.launcher3.Launcher.OnResumeCallback;
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.LoggerUtils;
41 import com.android.launcher3.logging.StatsLogManager;
42 import com.android.launcher3.model.AppLaunchTracker;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.ItemInfoWithIcon;
45 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
46 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
47 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
48 import com.android.launcher3.util.PackageManagerHelper;
49 import com.android.launcher3.util.Themes;
50 
51 import java.net.URISyntaxException;
52 import java.util.ArrayList;
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;
SecondaryDropTarget(Context context, AttributeSet attrs)71     public SecondaryDropTarget(Context context, AttributeSet attrs) {
72         this(context, attrs, 0);
73     }
74 
SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle)75     public SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle) {
76         super(context, attrs, defStyle);
77         mCacheExpireAlarm = new Alarm();
78         mStatsLogManager = StatsLogManager.newInstance(context);
79     }
80 
81     @Override
onAttachedToWindow()82     protected void onAttachedToWindow() {
83         super.onAttachedToWindow();
84         if (mHadPendingAlarm) {
85             mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
86             mCacheExpireAlarm.setOnAlarmListener(this);
87             mHadPendingAlarm = false;
88         }
89     }
90 
91     @Override
onDetachedFromWindow()92     protected void onDetachedFromWindow() {
93         super.onDetachedFromWindow();
94         if (mCacheExpireAlarm.alarmPending()) {
95             mCacheExpireAlarm.cancelAlarm();
96             mCacheExpireAlarm.setOnAlarmListener(null);
97             mHadPendingAlarm = true;
98         }
99     }
100 
101     @Override
onFinishInflate()102     protected void onFinishInflate() {
103         super.onFinishInflate();
104         setupUi(UNINSTALL);
105     }
106 
setupUi(int action)107     protected void setupUi(int action) {
108         if (action == mCurrentAccessibilityAction) {
109             return;
110         }
111         mCurrentAccessibilityAction = action;
112 
113         if (action == UNINSTALL) {
114             mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint);
115             setDrawable(R.drawable.ic_uninstall_shadow);
116             updateText(R.string.uninstall_drop_target_label);
117         } else if (action == DISMISS_PREDICTION) {
118             mHoverColor = Themes.getColorAccent(getContext());
119             setDrawable(R.drawable.ic_block_shadow);
120             updateText(R.string.dismiss_prediction_label);
121         } else if (action == RECONFIGURE) {
122             mHoverColor = Themes.getColorAccent(getContext());
123             setDrawable(R.drawable.ic_setup_shadow);
124             updateText(R.string.gadget_setup_text);
125         }
126     }
127 
128     @Override
onAlarm(Alarm alarm)129     public void onAlarm(Alarm alarm) {
130         mUninstallDisabledCache.clear();
131     }
132 
133     @Override
getAccessibilityAction()134     public int getAccessibilityAction() {
135         return mCurrentAccessibilityAction;
136     }
137 
138     @Override
getDropTargetForLogging()139     public Target getDropTargetForLogging() {
140         Target t = LoggerUtils.newTarget(Target.Type.CONTROL);
141         if (mCurrentAccessibilityAction == UNINSTALL) {
142             t.controlType = ControlType.UNINSTALL_TARGET;
143         } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
144             t.controlType = ControlType.DISMISS_PREDICTION;
145         } else {
146             t.controlType = ControlType.SETTINGS_BUTTON;
147         }
148         return t;
149     }
150 
151     @Override
supportsDrop(ItemInfo info)152     protected boolean supportsDrop(ItemInfo info) {
153         return supportsAccessibilityDrop(info, getViewUnderDrag(info));
154     }
155 
156     @Override
supportsAccessibilityDrop(ItemInfo info, View view)157     public boolean supportsAccessibilityDrop(ItemInfo info, View view) {
158         if (view instanceof AppWidgetHostView) {
159             if (getReconfigurableWidgetId(view) != INVALID_APPWIDGET_ID) {
160                 setupUi(RECONFIGURE);
161                 return true;
162             }
163             return false;
164         } else if (FeatureFlags.ENABLE_PREDICTION_DISMISS.get() && info.isPredictedItem()) {
165             setupUi(DISMISS_PREDICTION);
166             return true;
167         }
168 
169         setupUi(UNINSTALL);
170         Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user);
171         if (uninstallDisabled == null) {
172             UserManager userManager =
173                     (UserManager) getContext().getSystemService(Context.USER_SERVICE);
174             Bundle restrictions = userManager.getUserRestrictions(info.user);
175             uninstallDisabled = restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false)
176                     || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false);
177             mUninstallDisabledCache.put(info.user, uninstallDisabled);
178         }
179         // Cancel any pending alarm and set cache expiry after some time
180         mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
181         mCacheExpireAlarm.setOnAlarmListener(this);
182         if (uninstallDisabled) {
183             return false;
184         }
185 
186         if (info instanceof ItemInfoWithIcon) {
187             ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info;
188             if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0) {
189                 return (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) != 0;
190             }
191         }
192         return getUninstallTarget(info) != null;
193     }
194 
195     /**
196      * @return the component name that should be uninstalled or null.
197      */
getUninstallTarget(ItemInfo item)198     private ComponentName getUninstallTarget(ItemInfo item) {
199         Intent intent = null;
200         UserHandle user = null;
201         if (item != null &&
202                 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
203             intent = item.getIntent();
204             user = item.user;
205         }
206         if (intent != null) {
207             LauncherActivityInfo info = mLauncher.getSystemService(LauncherApps.class)
208                     .resolveActivity(intent, user);
209             if (info != null
210                     && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
211                 return info.getComponentName();
212             }
213         }
214         return null;
215     }
216 
217     @Override
onDrop(DragObject d, DragOptions options)218     public void onDrop(DragObject d, DragOptions options) {
219         // Defer onComplete
220         d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
221         super.onDrop(d, options);
222         if (mCurrentAccessibilityAction == UNINSTALL) {
223             mStatsLogManager.logger().withInstanceId(d.logInstanceId)
224                     .log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
225         } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
226             mStatsLogManager.logger().withInstanceId(d.logInstanceId)
227                     .log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST);
228         }
229     }
230 
231     @Override
completeDrop(final DragObject d)232     public void completeDrop(final DragObject d) {
233         ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo);
234         if (d.dragSource instanceof DeferredOnComplete) {
235             DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource;
236             if (target != null) {
237                 deferred.mPackageName = target.getPackageName();
238                 mLauncher.addOnResumeCallback(deferred);
239             } else {
240                 deferred.sendFailure();
241             }
242         }
243     }
244 
getViewUnderDrag(ItemInfo info)245     private View getViewUnderDrag(ItemInfo info) {
246         if (info instanceof LauncherAppWidgetInfo && info.container == CONTAINER_DESKTOP &&
247                 mLauncher.getWorkspace().getDragInfo() != null) {
248             return mLauncher.getWorkspace().getDragInfo().cell;
249         }
250         return null;
251     }
252 
253     /**
254      * Verifies that the view is an reconfigurable widget and returns the corresponding widget Id,
255      * otherwise return {@code INVALID_APPWIDGET_ID}
256      */
getReconfigurableWidgetId(View view)257     private int getReconfigurableWidgetId(View view) {
258         if (!(view instanceof AppWidgetHostView)) {
259             return INVALID_APPWIDGET_ID;
260         }
261         AppWidgetHostView hostView = (AppWidgetHostView) view;
262         AppWidgetProviderInfo widgetInfo = hostView.getAppWidgetInfo();
263         if (widgetInfo == null || widgetInfo.configure == null) {
264             return INVALID_APPWIDGET_ID;
265         }
266         if ( (LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), widgetInfo)
267                 .getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) == 0) {
268             return INVALID_APPWIDGET_ID;
269         }
270         return hostView.getAppWidgetId();
271     }
272 
273     /**
274      * Performs the drop action and returns the target component for the dragObject or null if
275      * the action was not performed.
276      */
performDropAction(View view, ItemInfo info)277     protected ComponentName performDropAction(View view, ItemInfo info) {
278         if (mCurrentAccessibilityAction == RECONFIGURE) {
279             int widgetId = getReconfigurableWidgetId(view);
280             if (widgetId != INVALID_APPWIDGET_ID) {
281                 mLauncher.getAppWidgetHost().startConfigActivity(mLauncher, widgetId, -1);
282             }
283             return null;
284         }
285         if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
286             AppLaunchTracker.INSTANCE.get(getContext()).onDismissApp(info.getTargetComponent(),
287                     info.user, AppLaunchTracker.CONTAINER_PREDICTIONS);
288             return null;
289         }
290         // else: mCurrentAccessibilityAction == UNINSTALL
291 
292         ComponentName cn = getUninstallTarget(info);
293         if (cn == null) {
294             // System applications cannot be installed. For now, show a toast explaining that.
295             // We may give them the option of disabling apps this way.
296             Toast.makeText(mLauncher, R.string.uninstall_system_app_text, Toast.LENGTH_SHORT).show();
297             return null;
298         }
299         try {
300             Intent i = Intent.parseUri(mLauncher.getString(R.string.delete_package_intent), 0)
301                     .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
302                     .putExtra(Intent.EXTRA_USER, info.user);
303             mLauncher.startActivity(i);
304             FileLog.d(TAG, "start uninstall activity " + cn.getPackageName());
305             return cn;
306         } catch (URISyntaxException e) {
307             Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info);
308             return null;
309         }
310     }
311 
312     @Override
onAccessibilityDrop(View view, ItemInfo item)313     public void onAccessibilityDrop(View view, ItemInfo item) {
314         performDropAction(view, item);
315     }
316 
317     /**
318      * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until
319      * {@link #onLauncherResume}
320      */
321     private class DeferredOnComplete implements DragSource, OnResumeCallback {
322 
323         private final DragSource mOriginal;
324         private final Context mContext;
325 
326         private String mPackageName;
327         private DragObject mDragObject;
328 
DeferredOnComplete(DragSource original, Context context)329         public DeferredOnComplete(DragSource original, Context context) {
330             mOriginal = original;
331             mContext = context;
332         }
333 
334         @Override
onDropCompleted(View target, DragObject d, boolean success)335         public void onDropCompleted(View target, DragObject d,
336                 boolean success) {
337             mDragObject = d;
338         }
339 
340         @Override
fillInLogContainerData(ItemInfo childInfo, Target child, ArrayList<Target> parents)341         public void fillInLogContainerData(ItemInfo childInfo, Target child,
342                 ArrayList<Target> parents) {
343             mOriginal.fillInLogContainerData(childInfo, child, parents);
344         }
345 
346         @Override
onLauncherResume()347         public void onLauncherResume() {
348             // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
349             if (new PackageManagerHelper(mContext).getApplicationInfo(mPackageName,
350                     mDragObject.dragInfo.user, PackageManager.MATCH_UNINSTALLED_PACKAGES) == null) {
351                 mDragObject.dragSource = mOriginal;
352                 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
353                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
354                         .log(LAUNCHER_ITEM_UNINSTALL_COMPLETED);
355             } else {
356                 sendFailure();
357                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
358                         .log(LAUNCHER_ITEM_UNINSTALL_CANCELLED);
359             }
360         }
361 
sendFailure()362         public void sendFailure() {
363             mDragObject.dragSource = mOriginal;
364             mDragObject.cancelled = true;
365             mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false);
366         }
367     }
368 }
369