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.launcher2;
18
19import android.animation.TimeInterpolator;
20import android.animation.ValueAnimator;
21import android.animation.ValueAnimator.AnimatorUpdateListener;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.graphics.PointF;
27import android.graphics.Rect;
28import android.graphics.drawable.TransitionDrawable;
29import android.os.UserManager;
30import android.util.AttributeSet;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewGroup;
34import android.view.animation.AnimationUtils;
35import android.view.animation.DecelerateInterpolator;
36import android.view.animation.LinearInterpolator;
37
38import com.android.launcher.R;
39
40public class DeleteDropTarget extends ButtonDropTarget {
41    private static int DELETE_ANIMATION_DURATION = 285;
42    private static int FLING_DELETE_ANIMATION_DURATION = 350;
43    private static float FLING_TO_DELETE_FRICTION = 0.035f;
44    private static int MODE_FLING_DELETE_TO_TRASH = 0;
45    private static int MODE_FLING_DELETE_ALONG_VECTOR = 1;
46
47    private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR;
48
49    private ColorStateList mOriginalTextColor;
50    private TransitionDrawable mUninstallDrawable;
51    private TransitionDrawable mRemoveDrawable;
52    private TransitionDrawable mCurrentDrawable;
53
54    public DeleteDropTarget(Context context, AttributeSet attrs) {
55        this(context, attrs, 0);
56    }
57
58    public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) {
59        super(context, attrs, defStyle);
60    }
61
62    @Override
63    protected void onFinishInflate() {
64        super.onFinishInflate();
65
66        // Get the drawable
67        mOriginalTextColor = getTextColors();
68
69        // Get the hover color
70        Resources r = getResources();
71        mHoverColor = r.getColor(R.color.delete_target_hover_tint);
72        mUninstallDrawable = (TransitionDrawable)
73                r.getDrawable(R.drawable.uninstall_target_selector);
74        mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector);
75
76        mRemoveDrawable.setCrossFadeEnabled(true);
77        mUninstallDrawable.setCrossFadeEnabled(true);
78
79        // The current drawable is set to either the remove drawable or the uninstall drawable
80        // and is initially set to the remove drawable, as set in the layout xml.
81        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
82
83        // Remove the text in the Phone UI in landscape
84        int orientation = getResources().getConfiguration().orientation;
85        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
86            if (!LauncherApplication.isScreenLarge()) {
87                setText("");
88            }
89        }
90    }
91
92    private boolean isAllAppsApplication(DragSource source, Object info) {
93        return (source instanceof AppsCustomizePagedView) && (info instanceof ApplicationInfo);
94    }
95    private boolean isAllAppsWidget(DragSource source, Object info) {
96        if (source instanceof AppsCustomizePagedView) {
97            if (info instanceof PendingAddItemInfo) {
98                PendingAddItemInfo addInfo = (PendingAddItemInfo) info;
99                switch (addInfo.itemType) {
100                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
101                    case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
102                        return true;
103                }
104            }
105        }
106        return false;
107    }
108    private boolean isDragSourceWorkspaceOrFolder(DragObject d) {
109        return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder);
110    }
111    private boolean isWorkspaceOrFolderApplication(DragObject d) {
112        return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo);
113    }
114    private boolean isWorkspaceOrFolderWidget(DragObject d) {
115        return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo);
116    }
117    private boolean isWorkspaceFolder(DragObject d) {
118        return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo);
119    }
120
121    private void setHoverColor() {
122        mCurrentDrawable.startTransition(mTransitionDuration);
123        setTextColor(mHoverColor);
124    }
125    private void resetHoverColor() {
126        mCurrentDrawable.resetTransition();
127        setTextColor(mOriginalTextColor);
128    }
129
130    @Override
131    public boolean acceptDrop(DragObject d) {
132        // We can remove everything including App shortcuts, folders, widgets, etc.
133        return true;
134    }
135
136    @Override
137    public void onDragStart(DragSource source, Object info, int dragAction) {
138        boolean isVisible = true;
139        boolean isUninstall = false;
140
141        // If we are dragging a widget from AppsCustomize, hide the delete target
142        if (isAllAppsWidget(source, info)) {
143            isVisible = false;
144        }
145
146        // If we are dragging an application from AppsCustomize, only show the control if we can
147        // delete the app (it was downloaded), and rename the string to "uninstall" in such a case
148        if (isAllAppsApplication(source, info)) {
149            ApplicationInfo appInfo = (ApplicationInfo) info;
150            if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) != 0) {
151                isUninstall = true;
152            } else {
153                isVisible = false;
154            }
155            // If the user is not allowed to access the app details page or uninstall, then don't
156            // let them uninstall from here either.
157            UserManager userManager = (UserManager)
158                    getContext().getSystemService(Context.USER_SERVICE);
159            if (userManager.hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL)
160                    || userManager.hasUserRestriction(UserManager.DISALLOW_UNINSTALL_APPS)) {
161                isVisible = false;
162            }
163        }
164
165        if (isUninstall) {
166            setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
167        } else {
168            setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
169        }
170        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
171
172        mActive = isVisible;
173        resetHoverColor();
174        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
175        if (getText().length() > 0) {
176            setText(isUninstall ? R.string.delete_target_uninstall_label
177                : R.string.delete_target_label);
178        }
179    }
180
181    @Override
182    public void onDragEnd() {
183        super.onDragEnd();
184        mActive = false;
185    }
186
187    public void onDragEnter(DragObject d) {
188        super.onDragEnter(d);
189
190        setHoverColor();
191    }
192
193    public void onDragExit(DragObject d) {
194        super.onDragExit(d);
195
196        if (!d.dragComplete) {
197            resetHoverColor();
198        } else {
199            // Restore the hover color if we are deleting
200            d.dragView.setColor(mHoverColor);
201        }
202    }
203
204    private void animateToTrashAndCompleteDrop(final DragObject d) {
205        DragLayer dragLayer = mLauncher.getDragLayer();
206        Rect from = new Rect();
207        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
208        Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
209                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
210        float scale = (float) to.width() / from.width();
211
212        mSearchDropTargetBar.deferOnDragEnd();
213        Runnable onAnimationEndRunnable = new Runnable() {
214            @Override
215            public void run() {
216                mSearchDropTargetBar.onDragEnd();
217                mLauncher.exitSpringLoadedDragMode();
218                completeDrop(d);
219            }
220        };
221        dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
222                DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2),
223                new LinearInterpolator(), onAnimationEndRunnable,
224                DragLayer.ANIMATION_END_DISAPPEAR, null);
225    }
226
227    private void completeDrop(DragObject d) {
228        ItemInfo item = (ItemInfo) d.dragInfo;
229
230        if (isAllAppsApplication(d.dragSource, item)) {
231            // Uninstall the application if it is being dragged from AppsCustomize
232            mLauncher.startApplicationUninstallActivity((ApplicationInfo) item, item.user);
233        } else if (isWorkspaceOrFolderApplication(d)) {
234            LauncherModel.deleteItemFromDatabase(mLauncher, item);
235        } else if (isWorkspaceFolder(d)) {
236            // Remove the folder from the workspace and delete the contents from launcher model
237            FolderInfo folderInfo = (FolderInfo) item;
238            mLauncher.removeFolder(folderInfo);
239            LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo);
240        } else if (isWorkspaceOrFolderWidget(d)) {
241            // Remove the widget from the workspace
242            mLauncher.removeAppWidget((LauncherAppWidgetInfo) item);
243            LauncherModel.deleteItemFromDatabase(mLauncher, item);
244
245            final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
246            final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
247            if (appWidgetHost != null) {
248                // Deleting an app widget ID is a void call but writes to disk before returning
249                // to the caller...
250                new Thread("deleteAppWidgetId") {
251                    public void run() {
252                        appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
253                    }
254                }.start();
255            }
256        }
257    }
258
259    public void onDrop(DragObject d) {
260        animateToTrashAndCompleteDrop(d);
261    }
262
263    /**
264     * Creates an animation from the current drag view to the delete trash icon.
265     */
266    private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
267            DragObject d, PointF vel, ViewConfiguration config) {
268        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
269                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
270        final Rect from = new Rect();
271        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
272
273        // Calculate how far along the velocity vector we should put the intermediate point on
274        // the bezier curve
275        float velocity = Math.abs(vel.length());
276        float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f));
277        int offsetY = (int) (-from.top * vp);
278        int offsetX = (int) (offsetY / (vel.y / vel.x));
279        final float y2 = from.top + offsetY;                        // intermediate t/l
280        final float x2 = from.left + offsetX;
281        final float x1 = from.left;                                 // drag view t/l
282        final float y1 = from.top;
283        final float x3 = to.left;                                   // delete target t/l
284        final float y3 = to.top;
285
286        final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() {
287            @Override
288            public float getInterpolation(float t) {
289                return t * t * t * t * t * t * t * t;
290            }
291        };
292        return new AnimatorUpdateListener() {
293            @Override
294            public void onAnimationUpdate(ValueAnimator animation) {
295                final DragView dragView = (DragView) dragLayer.getAnimatedView();
296                float t = ((Float) animation.getAnimatedValue()).floatValue();
297                float tp = scaleAlphaInterpolator.getInterpolation(t);
298                float initialScale = dragView.getInitialScale();
299                float finalAlpha = 0.5f;
300                float scale = dragView.getScaleX();
301                float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f;
302                float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f;
303                float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) +
304                        (t * t) * x3;
305                float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) +
306                        (t * t) * y3;
307
308                dragView.setTranslationX(x);
309                dragView.setTranslationY(y);
310                dragView.setScaleX(initialScale * (1f - tp));
311                dragView.setScaleY(initialScale * (1f - tp));
312                dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp));
313            }
314        };
315    }
316
317    /**
318     * Creates an animation from the current drag view along its current velocity vector.
319     * For this animation, the alpha runs for a fixed duration and we update the position
320     * progressively.
321     */
322    private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
323        private DragLayer mDragLayer;
324        private PointF mVelocity;
325        private Rect mFrom;
326        private long mPrevTime;
327        private boolean mHasOffsetForScale;
328        private float mFriction;
329
330        private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
331
332        public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from,
333                long startTime, float friction) {
334            mDragLayer = dragLayer;
335            mVelocity = vel;
336            mFrom = from;
337            mPrevTime = startTime;
338            mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction);
339        }
340
341        @Override
342        public void onAnimationUpdate(ValueAnimator animation) {
343            final DragView dragView = (DragView) mDragLayer.getAnimatedView();
344            float t = ((Float) animation.getAnimatedValue()).floatValue();
345            long curTime = AnimationUtils.currentAnimationTimeMillis();
346
347            if (!mHasOffsetForScale) {
348                mHasOffsetForScale = true;
349                float scale = dragView.getScaleX();
350                float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f;
351                float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f;
352
353                mFrom.left += xOffset;
354                mFrom.top += yOffset;
355            }
356
357            mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
358            mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
359
360            dragView.setTranslationX(mFrom.left);
361            dragView.setTranslationY(mFrom.top);
362            dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
363
364            mVelocity.x *= mFriction;
365            mVelocity.y *= mFriction;
366            mPrevTime = curTime;
367        }
368    };
369    private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer,
370            DragObject d, PointF vel, final long startTime, final int duration,
371            ViewConfiguration config) {
372        final Rect from = new Rect();
373        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
374
375        return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime,
376                FLING_TO_DELETE_FRICTION);
377    }
378
379    public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
380        final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
381
382        // Don't highlight the icon as it's animating
383        d.dragView.setColor(0);
384        d.dragView.updateInitialScaleToCurrentScale();
385        // Don't highlight the target if we are flinging from AllApps
386        if (isAllApps) {
387            resetHoverColor();
388        }
389
390        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
391            // Defer animating out the drop target if we are animating to it
392            mSearchDropTargetBar.deferOnDragEnd();
393            mSearchDropTargetBar.finishAnimations();
394        }
395
396        final ViewConfiguration config = ViewConfiguration.get(mLauncher);
397        final DragLayer dragLayer = mLauncher.getDragLayer();
398        final int duration = FLING_DELETE_ANIMATION_DURATION;
399        final long startTime = AnimationUtils.currentAnimationTimeMillis();
400
401        // NOTE: Because it takes time for the first frame of animation to actually be
402        // called and we expect the animation to be a continuation of the fling, we have
403        // to account for the time that has elapsed since the fling finished.  And since
404        // we don't have a startDelay, we will always get call to update when we call
405        // start() (which we want to ignore).
406        final TimeInterpolator tInterpolator = new TimeInterpolator() {
407            private int mCount = -1;
408            private float mOffset = 0f;
409
410            @Override
411            public float getInterpolation(float t) {
412                if (mCount < 0) {
413                    mCount++;
414                } else if (mCount == 0) {
415                    mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
416                            startTime) / duration);
417                    mCount++;
418                }
419                return Math.min(1f, mOffset + t);
420            }
421        };
422        AnimatorUpdateListener updateCb = null;
423        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
424            updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config);
425        } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) {
426            updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
427                    duration, config);
428        }
429        Runnable onAnimationEndRunnable = new Runnable() {
430            @Override
431            public void run() {
432                // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up
433                // itself, otherwise, complete the drop to initiate the deletion process
434                if (!isAllApps) {
435                    mLauncher.exitSpringLoadedDragMode();
436                    completeDrop(d);
437                }
438                mLauncher.getDragController().onDeferredEndFling(d);
439            }
440        };
441        dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
442                DragLayer.ANIMATION_END_DISAPPEAR, null);
443    }
444}
445