DeleteDropTarget.java revision 4346746ef37413168a5cbec46bb6db8f96b0123a
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher3;
18
19import android.animation.TimeInterpolator;
20import android.animation.ValueAnimator;
21import android.animation.ValueAnimator.AnimatorUpdateListener;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ResolveInfo;
26import android.content.res.ColorStateList;
27import android.content.res.Configuration;
28import android.content.res.Resources;
29import android.graphics.PointF;
30import android.graphics.Rect;
31import android.graphics.drawable.TransitionDrawable;
32import android.os.AsyncTask;
33import android.util.AttributeSet;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewGroup;
37import android.view.animation.AnimationUtils;
38import android.view.animation.DecelerateInterpolator;
39import android.view.animation.LinearInterpolator;
40
41import java.util.List;
42import java.util.Set;
43
44public class DeleteDropTarget extends ButtonDropTarget {
45    private static int DELETE_ANIMATION_DURATION = 285;
46    private static int FLING_DELETE_ANIMATION_DURATION = 350;
47    private static float FLING_TO_DELETE_FRICTION = 0.035f;
48    private static int MODE_FLING_DELETE_TO_TRASH = 0;
49    private static int MODE_FLING_DELETE_ALONG_VECTOR = 1;
50
51    private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR;
52
53    private ColorStateList mOriginalTextColor;
54    private TransitionDrawable mUninstallDrawable;
55    private TransitionDrawable mRemoveDrawable;
56    private TransitionDrawable mCurrentDrawable;
57
58    private boolean mWaitingForUninstall = false;
59
60    public DeleteDropTarget(Context context, AttributeSet attrs) {
61        this(context, attrs, 0);
62    }
63
64    public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) {
65        super(context, attrs, defStyle);
66    }
67
68    @Override
69    protected void onFinishInflate() {
70        super.onFinishInflate();
71
72        // Get the drawable
73        mOriginalTextColor = getTextColors();
74
75        // Get the hover color
76        Resources r = getResources();
77        mHoverColor = r.getColor(R.color.delete_target_hover_tint);
78        mUninstallDrawable = (TransitionDrawable)
79                r.getDrawable(R.drawable.uninstall_target_selector);
80        mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector);
81
82        mRemoveDrawable.setCrossFadeEnabled(true);
83        mUninstallDrawable.setCrossFadeEnabled(true);
84
85        // The current drawable is set to either the remove drawable or the uninstall drawable
86        // and is initially set to the remove drawable, as set in the layout xml.
87        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
88
89        // Remove the text in the Phone UI in landscape
90        int orientation = getResources().getConfiguration().orientation;
91        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
92            if (!LauncherAppState.getInstance().isScreenLarge()) {
93                setText("");
94            }
95        }
96    }
97
98    private boolean isAllAppsApplication(DragSource source, Object info) {
99        return (source instanceof AppsCustomizePagedView) && (info instanceof AppInfo);
100    }
101    private boolean isAllAppsWidget(DragSource source, Object info) {
102        if (source instanceof AppsCustomizePagedView) {
103            if (info instanceof PendingAddItemInfo) {
104                PendingAddItemInfo addInfo = (PendingAddItemInfo) info;
105                switch (addInfo.itemType) {
106                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
107                    case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
108                        return true;
109                }
110            }
111        }
112        return false;
113    }
114    private boolean isDragSourceWorkspaceOrFolder(DragObject d) {
115        return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder);
116    }
117    private boolean isWorkspaceOrFolderApplication(DragObject d) {
118        return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo);
119    }
120    private boolean isWorkspaceOrFolderWidget(DragObject d) {
121        return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo);
122    }
123    private boolean isWorkspaceFolder(DragObject d) {
124        return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo);
125    }
126
127    private void setHoverColor() {
128        mCurrentDrawable.startTransition(mTransitionDuration);
129        setTextColor(mHoverColor);
130    }
131    private void resetHoverColor() {
132        mCurrentDrawable.resetTransition();
133        setTextColor(mOriginalTextColor);
134    }
135
136    @Override
137    public boolean acceptDrop(DragObject d) {
138        return willAcceptDrop(d.dragInfo);
139    }
140
141    public static boolean willAcceptDrop(Object info) {
142        if (info instanceof ItemInfo) {
143            ItemInfo item = (ItemInfo) info;
144            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
145                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
146                return true;
147            }
148
149            if (!AppsCustomizePagedView.DISABLE_ALL_APPS &&
150                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
151                return true;
152            }
153
154            if (!AppsCustomizePagedView.DISABLE_ALL_APPS &&
155                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
156                    item instanceof AppInfo) {
157                AppInfo appInfo = (AppInfo) info;
158                return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0;
159            }
160
161            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
162                item instanceof ShortcutInfo) {
163                if (AppsCustomizePagedView.DISABLE_ALL_APPS) {
164                    ShortcutInfo shortcutInfo = (ShortcutInfo) info;
165                    return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0;
166                } else {
167                    return true;
168                }
169            }
170        }
171        return false;
172    }
173
174    @Override
175    public void onDragStart(DragSource source, Object info, int dragAction) {
176        boolean isVisible = true;
177        boolean useUninstallLabel = !AppsCustomizePagedView.DISABLE_ALL_APPS &&
178                isAllAppsApplication(source, info);
179
180        // If we are dragging an application from AppsCustomize, only show the control if we can
181        // delete the app (it was downloaded), and rename the string to "uninstall" in such a case.
182        // Hide the delete target if it is a widget from AppsCustomize.
183        if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) {
184            isVisible = false;
185        }
186
187        if (useUninstallLabel) {
188            setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
189        } else {
190            setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
191        }
192        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
193
194        mActive = isVisible;
195        resetHoverColor();
196        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
197        if (getText().length() > 0) {
198            setText(useUninstallLabel ? R.string.delete_target_uninstall_label
199                : R.string.delete_target_label);
200        }
201    }
202
203    @Override
204    public void onDragEnd() {
205        super.onDragEnd();
206        mActive = false;
207    }
208
209    public void onDragEnter(DragObject d) {
210        super.onDragEnter(d);
211
212        setHoverColor();
213    }
214
215    public void onDragExit(DragObject d) {
216        super.onDragExit(d);
217
218        if (!d.dragComplete) {
219            resetHoverColor();
220        } else {
221            // Restore the hover color if we are deleting
222            d.dragView.setColor(mHoverColor);
223        }
224    }
225
226    private void animateToTrashAndCompleteDrop(final DragObject d) {
227        final DragLayer dragLayer = mLauncher.getDragLayer();
228        final Rect from = new Rect();
229        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
230        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
231                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
232        final float scale = (float) to.width() / from.width();
233
234        mSearchDropTargetBar.deferOnDragEnd();
235        deferCompleteDropIfUninstalling(d);
236
237        Runnable onAnimationEndRunnable = new Runnable() {
238            @Override
239            public void run() {
240                completeDrop(d);
241                mSearchDropTargetBar.onDragEnd();
242                mLauncher.exitSpringLoadedDragMode();
243            }
244        };
245        dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
246                DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2),
247                new LinearInterpolator(), onAnimationEndRunnable,
248                DragLayer.ANIMATION_END_DISAPPEAR, null);
249    }
250
251    private void deferCompleteDropIfUninstalling(DragObject d) {
252        mWaitingForUninstall = false;
253        if (isUninstallFromWorkspace(d)) {
254            if (d.dragSource instanceof Folder) {
255                ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity();
256            } else if (d.dragSource instanceof Workspace) {
257                ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity();
258            }
259            mWaitingForUninstall = true;
260        }
261    }
262
263    private boolean isUninstallFromWorkspace(DragObject d) {
264        if (AppsCustomizePagedView.DISABLE_ALL_APPS && isWorkspaceOrFolderApplication(d)) {
265            ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo;
266            if (shortcut.intent != null && shortcut.intent.getComponent() != null) {
267                Set<String> categories = shortcut.intent.getCategories();
268                boolean includesLauncherCategory = false;
269                if (categories != null) {
270                    for (String category : categories) {
271                        if (category.equals(Intent.CATEGORY_LAUNCHER)) {
272                            includesLauncherCategory = true;
273                            break;
274                        }
275                    }
276                }
277                return includesLauncherCategory;
278            }
279        }
280        return false;
281    }
282
283    private void completeDrop(DragObject d) {
284        ItemInfo item = (ItemInfo) d.dragInfo;
285        boolean wasWaitingForUninstall = mWaitingForUninstall;
286        mWaitingForUninstall = false;
287        if (isAllAppsApplication(d.dragSource, item)) {
288            // Uninstall the application if it is being dragged from AppsCustomize
289            AppInfo appInfo = (AppInfo) item;
290            mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags);
291        } else if (isUninstallFromWorkspace(d)) {
292            ShortcutInfo shortcut = (ShortcutInfo) item;
293            if (shortcut.intent != null && shortcut.intent.getComponent() != null) {
294                final ComponentName componentName = shortcut.intent.getComponent();
295                final DragSource dragSource = d.dragSource;
296                int flags = AppInfo.initFlags(
297                    ShortcutInfo.getPackageInfo(getContext(), componentName.getPackageName()));
298                mWaitingForUninstall =
299                    mLauncher.startApplicationUninstallActivity(componentName, flags);
300                if (mWaitingForUninstall) {
301                    final Runnable checkIfUninstallWasSuccess = new Runnable() {
302                        @Override
303                        public void run() {
304                            mWaitingForUninstall = false;
305                            String packageName = componentName.getPackageName();
306                            List<ResolveInfo> activities =
307                                    AllAppsList.findActivitiesForPackage(getContext(), packageName);
308                            boolean uninstallSuccessful = activities.size() == 0;
309                            if (dragSource instanceof Folder) {
310                                ((Folder) dragSource).
311                                    onUninstallActivityReturned(uninstallSuccessful);
312                            } else if (dragSource instanceof Workspace) {
313                                ((Workspace) dragSource).
314                                    onUninstallActivityReturned(uninstallSuccessful);
315                            }
316                        }
317                    };
318                    mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess);
319                }
320            }
321        } else if (isWorkspaceOrFolderApplication(d)) {
322            LauncherModel.deleteItemFromDatabase(mLauncher, item);
323        } else if (isWorkspaceFolder(d)) {
324            // Remove the folder from the workspace and delete the contents from launcher model
325            FolderInfo folderInfo = (FolderInfo) item;
326            mLauncher.removeFolder(folderInfo);
327            LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo);
328        } else if (isWorkspaceOrFolderWidget(d)) {
329            // Remove the widget from the workspace
330            mLauncher.removeAppWidget((LauncherAppWidgetInfo) item);
331            LauncherModel.deleteItemFromDatabase(mLauncher, item);
332
333            final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
334            final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
335            if (appWidgetHost != null) {
336                // Deleting an app widget ID is a void call but writes to disk before returning
337                // to the caller...
338                new AsyncTask<Void, Void, Void>() {
339                    public Void doInBackground(Void ... args) {
340                        appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
341                        return null;
342                    }
343                }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
344            }
345        }
346        if (wasWaitingForUninstall && !mWaitingForUninstall) {
347            if (d.dragSource instanceof Folder) {
348                ((Folder) d.dragSource).onUninstallActivityReturned(false);
349            } else if (d.dragSource instanceof Workspace) {
350                ((Workspace) d.dragSource).onUninstallActivityReturned(false);
351            }
352        }
353    }
354
355    public void onDrop(DragObject d) {
356        animateToTrashAndCompleteDrop(d);
357    }
358
359    /**
360     * Creates an animation from the current drag view to the delete trash icon.
361     */
362    private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
363            DragObject d, PointF vel, ViewConfiguration config) {
364        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
365                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
366        final Rect from = new Rect();
367        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
368
369        // Calculate how far along the velocity vector we should put the intermediate point on
370        // the bezier curve
371        float velocity = Math.abs(vel.length());
372        float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f));
373        int offsetY = (int) (-from.top * vp);
374        int offsetX = (int) (offsetY / (vel.y / vel.x));
375        final float y2 = from.top + offsetY;                        // intermediate t/l
376        final float x2 = from.left + offsetX;
377        final float x1 = from.left;                                 // drag view t/l
378        final float y1 = from.top;
379        final float x3 = to.left;                                   // delete target t/l
380        final float y3 = to.top;
381
382        final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() {
383            @Override
384            public float getInterpolation(float t) {
385                return t * t * t * t * t * t * t * t;
386            }
387        };
388        return new AnimatorUpdateListener() {
389            @Override
390            public void onAnimationUpdate(ValueAnimator animation) {
391                final DragView dragView = (DragView) dragLayer.getAnimatedView();
392                float t = ((Float) animation.getAnimatedValue()).floatValue();
393                float tp = scaleAlphaInterpolator.getInterpolation(t);
394                float initialScale = dragView.getInitialScale();
395                float finalAlpha = 0.5f;
396                float scale = dragView.getScaleX();
397                float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f;
398                float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f;
399                float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) +
400                        (t * t) * x3;
401                float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) +
402                        (t * t) * y3;
403
404                dragView.setTranslationX(x);
405                dragView.setTranslationY(y);
406                dragView.setScaleX(initialScale * (1f - tp));
407                dragView.setScaleY(initialScale * (1f - tp));
408                dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp));
409            }
410        };
411    }
412
413    /**
414     * Creates an animation from the current drag view along its current velocity vector.
415     * For this animation, the alpha runs for a fixed duration and we update the position
416     * progressively.
417     */
418    private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
419        private DragLayer mDragLayer;
420        private PointF mVelocity;
421        private Rect mFrom;
422        private long mPrevTime;
423        private boolean mHasOffsetForScale;
424        private float mFriction;
425
426        private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
427
428        public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from,
429                long startTime, float friction) {
430            mDragLayer = dragLayer;
431            mVelocity = vel;
432            mFrom = from;
433            mPrevTime = startTime;
434            mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction);
435        }
436
437        @Override
438        public void onAnimationUpdate(ValueAnimator animation) {
439            final DragView dragView = (DragView) mDragLayer.getAnimatedView();
440            float t = ((Float) animation.getAnimatedValue()).floatValue();
441            long curTime = AnimationUtils.currentAnimationTimeMillis();
442
443            if (!mHasOffsetForScale) {
444                mHasOffsetForScale = true;
445                float scale = dragView.getScaleX();
446                float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f;
447                float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f;
448
449                mFrom.left += xOffset;
450                mFrom.top += yOffset;
451            }
452
453            mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
454            mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
455
456            dragView.setTranslationX(mFrom.left);
457            dragView.setTranslationY(mFrom.top);
458            dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
459
460            mVelocity.x *= mFriction;
461            mVelocity.y *= mFriction;
462            mPrevTime = curTime;
463        }
464    };
465    private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer,
466            DragObject d, PointF vel, final long startTime, final int duration,
467            ViewConfiguration config) {
468        final Rect from = new Rect();
469        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
470
471        return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime,
472                FLING_TO_DELETE_FRICTION);
473    }
474
475    public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
476        final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
477
478        // Don't highlight the icon as it's animating
479        d.dragView.setColor(0);
480        d.dragView.updateInitialScaleToCurrentScale();
481        // Don't highlight the target if we are flinging from AllApps
482        if (isAllApps) {
483            resetHoverColor();
484        }
485
486        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
487            // Defer animating out the drop target if we are animating to it
488            mSearchDropTargetBar.deferOnDragEnd();
489            mSearchDropTargetBar.finishAnimations();
490        }
491
492        final ViewConfiguration config = ViewConfiguration.get(mLauncher);
493        final DragLayer dragLayer = mLauncher.getDragLayer();
494        final int duration = FLING_DELETE_ANIMATION_DURATION;
495        final long startTime = AnimationUtils.currentAnimationTimeMillis();
496
497        // NOTE: Because it takes time for the first frame of animation to actually be
498        // called and we expect the animation to be a continuation of the fling, we have
499        // to account for the time that has elapsed since the fling finished.  And since
500        // we don't have a startDelay, we will always get call to update when we call
501        // start() (which we want to ignore).
502        final TimeInterpolator tInterpolator = new TimeInterpolator() {
503            private int mCount = -1;
504            private float mOffset = 0f;
505
506            @Override
507            public float getInterpolation(float t) {
508                if (mCount < 0) {
509                    mCount++;
510                } else if (mCount == 0) {
511                    mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
512                            startTime) / duration);
513                    mCount++;
514                }
515                return Math.min(1f, mOffset + t);
516            }
517        };
518        AnimatorUpdateListener updateCb = null;
519        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
520            updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config);
521        } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) {
522            updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
523                    duration, config);
524        }
525        deferCompleteDropIfUninstalling(d);
526
527        Runnable onAnimationEndRunnable = new Runnable() {
528            @Override
529            public void run() {
530                // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up
531                // itself, otherwise, complete the drop to initiate the deletion process
532                if (!isAllApps) {
533                    mLauncher.exitSpringLoadedDragMode();
534                    completeDrop(d);
535                }
536                mLauncher.getDragController().onDeferredEndFling(d);
537            }
538        };
539        dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
540                DragLayer.ANIMATION_END_DISAPPEAR, null);
541    }
542}
543