1package com.android.launcher3;
2
3import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
4import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
5
6import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
7import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO;
8import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
9import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE;
10import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL;
11
12import android.appwidget.AppWidgetHostView;
13import android.appwidget.AppWidgetManager;
14import android.appwidget.AppWidgetProviderInfo;
15import android.content.ComponentName;
16import android.content.Context;
17import android.content.Intent;
18import android.content.pm.ApplicationInfo;
19import android.content.pm.LauncherActivityInfo;
20import android.content.pm.PackageManager;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.UserHandle;
24import android.os.UserManager;
25import android.util.ArrayMap;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.View;
29import android.widget.Toast;
30
31import com.android.launcher3.Launcher.OnResumeCallback;
32import com.android.launcher3.compat.LauncherAppsCompat;
33import com.android.launcher3.dragndrop.DragOptions;
34import com.android.launcher3.logging.LoggerUtils;
35import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
36import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
37import com.android.launcher3.util.Themes;
38
39import java.net.URISyntaxException;
40
41/**
42 * Drop target which provides a secondary option for an item.
43 *    For app targets: shows as uninstall
44 *    For configurable widgets: shows as setup
45 */
46public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmListener {
47
48    private static final String TAG = "SecondaryDropTarget";
49
50    private static final long CACHE_EXPIRE_TIMEOUT = 5000;
51    private final ArrayMap<UserHandle, Boolean> mUninstallDisabledCache = new ArrayMap<>(1);
52
53    private final Alarm mCacheExpireAlarm;
54
55    protected int mCurrentAccessibilityAction = -1;
56    public SecondaryDropTarget(Context context, AttributeSet attrs) {
57        this(context, attrs, 0);
58    }
59
60    public SecondaryDropTarget(Context context, AttributeSet attrs, int defStyle) {
61        super(context, attrs, defStyle);
62
63        mCacheExpireAlarm = new Alarm();
64        mCacheExpireAlarm.setOnAlarmListener(this);
65    }
66
67    @Override
68    protected void onFinishInflate() {
69        super.onFinishInflate();
70        setupUi(UNINSTALL);
71    }
72
73    protected void setupUi(int action) {
74        if (action == mCurrentAccessibilityAction) {
75            return;
76        }
77        mCurrentAccessibilityAction = action;
78
79        if (action == UNINSTALL) {
80            mHoverColor = getResources().getColor(R.color.uninstall_target_hover_tint);
81            setDrawable(R.drawable.ic_uninstall_shadow);
82            updateText(R.string.uninstall_drop_target_label);
83        } else {
84            mHoverColor = Themes.getColorAccent(getContext());
85            setDrawable(R.drawable.ic_setup_shadow);
86            updateText(R.string.gadget_setup_text);
87        }
88    }
89
90    @Override
91    public void onAlarm(Alarm alarm) {
92        mUninstallDisabledCache.clear();
93    }
94
95    @Override
96    public int getAccessibilityAction() {
97        return mCurrentAccessibilityAction;
98    }
99
100    @Override
101    public Target getDropTargetForLogging() {
102        Target t = LoggerUtils.newTarget(Target.Type.CONTROL);
103        t.controlType = mCurrentAccessibilityAction == UNINSTALL ? ControlType.UNINSTALL_TARGET
104                : ControlType.SETTINGS_BUTTON;
105        return t;
106    }
107
108    @Override
109    protected boolean supportsDrop(ItemInfo info) {
110        return supportsAccessibilityDrop(info, getViewUnderDrag(info));
111    }
112
113    @Override
114    public boolean supportsAccessibilityDrop(ItemInfo info, View view) {
115        if (view instanceof AppWidgetHostView) {
116            if (getReconfigurableWidgetId(view) != INVALID_APPWIDGET_ID) {
117                setupUi(RECONFIGURE);
118                return true;
119            }
120            return false;
121        }
122
123        setupUi(UNINSTALL);
124        Boolean uninstallDisabled = mUninstallDisabledCache.get(info.user);
125        if (uninstallDisabled == null) {
126            UserManager userManager =
127                    (UserManager) getContext().getSystemService(Context.USER_SERVICE);
128            Bundle restrictions = userManager.getUserRestrictions(info.user);
129            uninstallDisabled = restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false)
130                    || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false);
131            mUninstallDisabledCache.put(info.user, uninstallDisabled);
132        }
133        // Cancel any pending alarm and set cache expiry after some time
134        mCacheExpireAlarm.setAlarm(CACHE_EXPIRE_TIMEOUT);
135        if (uninstallDisabled) {
136            return false;
137        }
138
139        if (info instanceof ItemInfoWithIcon) {
140            ItemInfoWithIcon iconInfo = (ItemInfoWithIcon) info;
141            if ((iconInfo.runtimeStatusFlags & FLAG_SYSTEM_MASK) != 0) {
142                return (iconInfo.runtimeStatusFlags & FLAG_SYSTEM_NO) != 0;
143            }
144        }
145        return getUninstallTarget(info) != null;
146    }
147
148    /**
149     * @return the component name that should be uninstalled or null.
150     */
151    private ComponentName getUninstallTarget(ItemInfo item) {
152        Intent intent = null;
153        UserHandle user = null;
154        if (item != null &&
155                item.itemType == LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION) {
156            intent = item.getIntent();
157            user = item.user;
158        }
159        if (intent != null) {
160            LauncherActivityInfo info = LauncherAppsCompat.getInstance(mLauncher)
161                    .resolveActivity(intent, user);
162            if (info != null
163                    && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
164                return info.getComponentName();
165            }
166        }
167        return null;
168    }
169
170    @Override
171    public void onDrop(DragObject d, DragOptions options) {
172        // Defer onComplete
173        d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
174        super.onDrop(d, options);
175    }
176
177    @Override
178    public void completeDrop(final DragObject d) {
179        ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo);
180        if (d.dragSource instanceof DeferredOnComplete) {
181            DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource;
182            if (target != null) {
183                deferred.mPackageName = target.getPackageName();
184                mLauncher.setOnResumeCallback(deferred);
185            } else {
186                deferred.sendFailure();
187            }
188        }
189    }
190
191    private View getViewUnderDrag(ItemInfo info) {
192        if (info instanceof LauncherAppWidgetInfo && info.container == CONTAINER_DESKTOP &&
193                mLauncher.getWorkspace().getDragInfo() != null) {
194            return mLauncher.getWorkspace().getDragInfo().cell;
195        }
196        return null;
197    }
198
199    /**
200     * Verifies that the view is an reconfigurable widget and returns the corresponding widget Id,
201     * otherwise return {@code INVALID_APPWIDGET_ID}
202     */
203    private int getReconfigurableWidgetId(View view) {
204        if (!(view instanceof AppWidgetHostView)) {
205            return INVALID_APPWIDGET_ID;
206        }
207        AppWidgetHostView hostView = (AppWidgetHostView) view;
208        AppWidgetProviderInfo widgetInfo = hostView.getAppWidgetInfo();
209        if (widgetInfo == null || widgetInfo.configure == null) {
210            return INVALID_APPWIDGET_ID;
211        }
212        if ( (LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(), widgetInfo)
213                .getWidgetFeatures() & WIDGET_FEATURE_RECONFIGURABLE) == 0) {
214            return INVALID_APPWIDGET_ID;
215        }
216        return hostView.getAppWidgetId();
217    }
218
219    /**
220     * Performs the drop action and returns the target component for the dragObject or null if
221     * the action was not performed.
222     */
223    protected ComponentName performDropAction(View view, ItemInfo info) {
224        if (mCurrentAccessibilityAction == RECONFIGURE) {
225            int widgetId = getReconfigurableWidgetId(view);
226            if (widgetId != INVALID_APPWIDGET_ID) {
227                mLauncher.getAppWidgetHost().startConfigActivity(mLauncher, widgetId, -1);
228            }
229            return null;
230        }
231        // else: mCurrentAccessibilityAction == UNINSTALL
232
233        ComponentName cn = getUninstallTarget(info);
234        if (cn == null) {
235            // System applications cannot be installed. For now, show a toast explaining that.
236            // We may give them the option of disabling apps this way.
237            Toast.makeText(mLauncher, R.string.uninstall_system_app_text, Toast.LENGTH_SHORT).show();
238            return null;
239        }
240        try {
241            Intent i = Intent.parseUri(mLauncher.getString(R.string.delete_package_intent), 0)
242                    .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName()))
243                    .putExtra(Intent.EXTRA_USER, info.user);
244            mLauncher.startActivity(i);
245            return cn;
246        } catch (URISyntaxException e) {
247            Log.e(TAG, "Failed to parse intent to start uninstall activity for item=" + info);
248            return null;
249        }
250    }
251
252    @Override
253    public void onAccessibilityDrop(View view, ItemInfo item) {
254        performDropAction(view, item);
255    }
256
257    /**
258     * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until
259     * {@link #onLauncherResume}
260     */
261    private class DeferredOnComplete implements DragSource, OnResumeCallback {
262
263        private final DragSource mOriginal;
264        private final Context mContext;
265
266        private String mPackageName;
267        private DragObject mDragObject;
268
269        public DeferredOnComplete(DragSource original, Context context) {
270            mOriginal = original;
271            mContext = context;
272        }
273
274        @Override
275        public void onDropCompleted(View target, DragObject d,
276                boolean success) {
277            mDragObject = d;
278        }
279
280        @Override
281        public void fillInLogContainerData(View v, ItemInfo info, Target target,
282                Target targetParent) {
283            mOriginal.fillInLogContainerData(v, info, target, targetParent);
284        }
285
286        @Override
287        public void onLauncherResume() {
288            // We use MATCH_UNINSTALLED_PACKAGES as the app can be on SD card as well.
289            if (LauncherAppsCompat.getInstance(mContext)
290                    .getApplicationInfo(mPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES,
291                            mDragObject.dragInfo.user) == null) {
292                mDragObject.dragSource = mOriginal;
293                mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, true);
294            } else {
295                sendFailure();
296            }
297        }
298
299        public void sendFailure() {
300            mDragObject.dragSource = mOriginal;
301            mDragObject.cancelled = true;
302            mOriginal.onDropCompleted(SecondaryDropTarget.this, mDragObject, false);
303        }
304    }
305}
306