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