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