• 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_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.net.Uri;
26 import android.os.Bundle;
27 import android.os.UserHandle;
28 import android.os.UserManager;
29 import android.util.ArrayMap;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.Toast;
34 
35 import androidx.annotation.Nullable;
36 
37 import com.android.launcher3.dragndrop.DragOptions;
38 import com.android.launcher3.logging.FileLog;
39 import com.android.launcher3.logging.InstanceId;
40 import com.android.launcher3.logging.InstanceIdSequence;
41 import com.android.launcher3.logging.StatsLogManager;
42 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.ItemInfoWithIcon;
45 import com.android.launcher3.pm.UserCache;
46 import com.android.launcher3.util.ApplicationInfoWrapper;
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             if (Flags.enableShortcutDontSuggestApp()) {
159                 return INVALID;
160             }
161             return DISMISS_PREDICTION;
162         }
163 
164         Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user);
165         if (uninstallDisabled == null) {
166             UserManager userManager =
167                     (UserManager) getContext().getSystemService(Context.USER_SERVICE);
168             Bundle restrictions = userManager.getUserRestrictions(info.user);
169             uninstallDisabled = restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false)
170                     || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false);
171             mUninstallDisabledCache.put(info.user, uninstallDisabled);
172         }
173         // Cancel any pending alarm and set cache expiry after some time
174         mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
175         mCacheExpireAlarm.setOnAlarmListener(this);
176         if (uninstallDisabled) {
177             return INVALID;
178         }
179         if (Flags.enablePrivateSpace() && UserCache.getInstance(getContext()).getUserInfo(
180                 info.user).isPrivate()) {
181             return INVALID;
182         }
183 
184         if (info instanceof ItemInfoWithIcon) {
185             ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info;
186             if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0
187                     && (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) == 0) {
188                 return INVALID;
189             }
190         }
191         if (getUninstallTarget(getContext(), info) == null) {
192             return INVALID;
193         }
194         return UNINSTALL;
195     }
196 
197     /**
198      * @return the component name that should be uninstalled or null.
199      */
getUninstallTarget(Context context, ItemInfo item)200     public static ComponentName getUninstallTarget(Context context, ItemInfo item) {
201         Intent intent = null;
202         UserHandle user = null;
203         if (item != null &&
204                 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
205             intent = item.getIntent();
206             user = item.user;
207         }
208         if (intent != null) {
209             LauncherActivityInfo info = context.getSystemService(LauncherApps.class)
210                     .resolveActivity(intent, user);
211             if (info != null
212                     && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
213                 return info.getComponentName();
214             }
215         }
216         return null;
217     }
218 
219     @Override
onDrop(DragObject d, DragOptions options)220     public void onDrop(DragObject d, DragOptions options) {
221         // Defer onComplete
222         d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
223 
224         super.onDrop(d, options);
225         doLog(d.logInstanceId, d.originalDragInfo);
226     }
227 
doLog(InstanceId logInstanceId, ItemInfo itemInfo)228     private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) {
229         StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId);
230         if (itemInfo != null) {
231             logger.withItemInfo(itemInfo);
232         }
233         if (mCurrentAccessibilityAction == UNINSTALL) {
234             logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
235         } else if (mCurrentAccessibilityAction == DISMISS_PREDICTION) {
236             logger.log(LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST);
237         }
238     }
239 
240     @Override
completeDrop(final DragObject d)241     public void completeDrop(final DragObject d) {
242         ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo);
243         mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d);
244     }
245 
getViewUnderDrag(ItemInfo info)246     private View getViewUnderDrag(ItemInfo info) {
247         return mDropTargetHandler.getViewUnderDrag(info);
248     }
249 
250     /**
251      * Verifies that the view is an reconfigurable widget and returns the corresponding widget Id,
252      * otherwise return {@code INVALID_APPWIDGET_ID}
253      */
getReconfigurableWidgetId(View view)254     private int getReconfigurableWidgetId(View view) {
255         if (!(view instanceof AppWidgetHostView)) {
256             return INVALID_APPWIDGET_ID;
257         }
258         AppWidgetHostView hostView = (AppWidgetHostView) view;
259         AppWidgetProviderInfo widgetInfo = hostView.getAppWidgetInfo();
260         if (widgetInfo == null || widgetInfo.configure == null) {
261             return INVALID_APPWIDGET_ID;
262         }
263         if ( (LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), widgetInfo)
264                 .getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) == 0) {
265             return INVALID_APPWIDGET_ID;
266         }
267         return hostView.getAppWidgetId();
268     }
269 
270     /**
271      * Performs the drop action and returns the target component for the dragObject or null if
272      * the action was not performed.
273      */
performDropAction(View view, ItemInfo info)274     protected ComponentName performDropAction(View view, ItemInfo info) {
275         if (mCurrentAccessibilityAction == RECONFIGURE) {
276             int widgetId = getReconfigurableWidgetId(view);
277             if (widgetId != INVALID_APPWIDGET_ID) {
278                 mDropTargetHandler.reconfigureWidget(widgetId, info);
279             }
280             return null;
281         }
282         return performUninstall(getContext(), getUninstallTarget(getContext(), info), info);
283     }
284 
285     /**
286      * Performs uninstall and returns the target component for the {@link ItemInfo} or null if
287      * the uninstall was not performed.
288      */
performUninstall(Context context, @Nullable ComponentName cn, ItemInfo info)289     public static ComponentName performUninstall(Context context, @Nullable ComponentName cn,
290             ItemInfo 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                     context,
296                     R.string.uninstall_system_app_text,
297                     Toast.LENGTH_SHORT
298             ).show();
299             return null;
300         }
301         try {
302             Intent i = Intent.parseUri(context.getString(R.string.delete_package_intent), 0)
303                     .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
304                     .putExtra(Intent.EXTRA_USER, info.user);
305             context.startActivity(i);
306             FileLog.d(TAG, "start uninstall activity from drop target " + cn.getPackageName());
307             return cn;
308         } catch (URISyntaxException e) {
309             Log.e(TAG, "Failed to parse intent to start drop target uninstall activity for"
310                     + " item=" + info);
311             return null;
312         }
313     }
314 
315     @Override
onAccessibilityDrop(View view, ItemInfo item)316     public void onAccessibilityDrop(View view, ItemInfo item) {
317         doLog(new InstanceIdSequence().newInstanceId(), item);
318         performDropAction(view, item);
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             if (new ApplicationInfoWrapper(mContext, mPackageName, mDragObject.dragInfo.user)
346                     .getInfo() == null) {
347                 mDragObject.dragSource = mOriginal;
348                 mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
349                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
350                         .log(LAUNCHER_ITEM_UNINSTALL_COMPLETED);
351             } else {
352                 sendFailure();
353                 mStatsLogManager.logger().withInstanceId(mDragObject.logInstanceId)
354                         .log(LAUNCHER_ITEM_UNINSTALL_CANCELLED);
355             }
356         }
357 
sendFailure()358         public void sendFailure() {
359             mDragObject.dragSource = mOriginal;
360             mDragObject.cancelled = true;
361             mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false);
362         }
363     }
364 }
365