1/*
2 * Copyright (C) 2017 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.dragndrop;
18
19import android.appwidget.AppWidgetManager;
20import android.content.ClipDescription;
21import android.content.Intent;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.os.SystemClock;
30import android.util.Log;
31import android.view.DragEvent;
32import android.view.View;
33import android.widget.RemoteViews;
34
35import com.android.launcher3.DeleteDropTarget;
36import com.android.launcher3.DragSource;
37import com.android.launcher3.DropTarget;
38import com.android.launcher3.ItemInfo;
39import com.android.launcher3.Launcher;
40import com.android.launcher3.LauncherAppWidgetProviderInfo;
41import com.android.launcher3.PendingAddItemInfo;
42import com.android.launcher3.R;
43import com.android.launcher3.compat.PinItemRequestCompat;
44import com.android.launcher3.folder.Folder;
45import com.android.launcher3.userevent.nano.LauncherLogProto;
46import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
47import com.android.launcher3.widget.PendingAddShortcutInfo;
48import com.android.launcher3.widget.PendingAddWidgetInfo;
49import com.android.launcher3.widget.PendingItemDragHelper;
50import com.android.launcher3.widget.WidgetAddFlowHandler;
51
52import java.util.UUID;
53
54/**
55 * {@link DragSource} for handling drop from a different window. This object is initialized
56 * in the source window and is passed on to the Launcher activity as an Intent extra.
57 */
58public class PinItemDragListener
59        implements Parcelable, View.OnDragListener, DragSource, DragOptions.PreDragCondition {
60
61    private static final String TAG = "PinItemDragListener";
62
63    private static final String MIME_TYPE_PREFIX = "com.android.launcher3.drag_and_drop/";
64    public static final String EXTRA_PIN_ITEM_DRAG_LISTENER = "pin_item_drag_listener";
65
66    private final PinItemRequestCompat mRequest;
67
68    // Position of preview relative to the touch location
69    private final Rect mPreviewRect;
70
71    private final int mPreviewBitmapWidth;
72    private final int mPreviewViewWidth;
73
74    // Randomly generated id used to verify the drag event.
75    private final String mId;
76
77    private Launcher mLauncher;
78    private DragController mDragController;
79    private long mDragStartTime;
80
81    public PinItemDragListener(PinItemRequestCompat request, Rect previewRect,
82            int previewBitmapWidth, int previewViewWidth) {
83        mRequest = request;
84        mPreviewRect = previewRect;
85        mPreviewBitmapWidth = previewBitmapWidth;
86        mPreviewViewWidth = previewViewWidth;
87        mId = UUID.randomUUID().toString();
88    }
89
90    private PinItemDragListener(Parcel parcel) {
91        mRequest = PinItemRequestCompat.CREATOR.createFromParcel(parcel);
92        mPreviewRect = Rect.CREATOR.createFromParcel(parcel);
93        mPreviewBitmapWidth = parcel.readInt();
94        mPreviewViewWidth = parcel.readInt();
95        mId = parcel.readString();
96    }
97
98    public String getMimeType() {
99        return MIME_TYPE_PREFIX + mId;
100    }
101
102    @Override
103    public int describeContents() {
104        return 0;
105    }
106
107    @Override
108    public void writeToParcel(Parcel parcel, int i) {
109        mRequest.writeToParcel(parcel, i);
110        mPreviewRect.writeToParcel(parcel, i);
111        parcel.writeInt(mPreviewBitmapWidth);
112        parcel.writeInt(mPreviewViewWidth);
113        parcel.writeString(mId);
114    }
115
116    public void setLauncher(Launcher launcher) {
117        mLauncher = launcher;
118        mDragController = launcher.getDragController();
119    }
120
121    @Override
122    public boolean onDrag(View view, DragEvent event) {
123        if (mLauncher == null || mDragController == null) {
124            postCleanup();
125            return false;
126        }
127        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
128            if (onDragStart(event)) {
129                return true;
130            } else {
131                postCleanup();
132                return false;
133            }
134        }
135        return mDragController.onDragEvent(mDragStartTime, event);
136    }
137
138    private boolean onDragStart(DragEvent event) {
139        if (!mRequest.isValid()) {
140            return false;
141        }
142        ClipDescription desc =  event.getClipDescription();
143        if (desc == null || !desc.hasMimeType(getMimeType())) {
144            Log.e(TAG, "Someone started a dragAndDrop before us.");
145            return false;
146        }
147
148        final PendingAddItemInfo item;
149        if (mRequest.getRequestType() == PinItemRequestCompat.REQUEST_TYPE_SHORTCUT) {
150            item = new PendingAddShortcutInfo(
151                    new PinShortcutRequestActivityInfo(mRequest, mLauncher));
152        } else {
153            // mRequest.getRequestType() == PinItemRequestCompat.REQUEST_TYPE_APPWIDGET
154            LauncherAppWidgetProviderInfo providerInfo =
155                    LauncherAppWidgetProviderInfo.fromProviderInfo(
156                            mLauncher, mRequest.getAppWidgetProviderInfo(mLauncher));
157            final PinWidgetFlowHandler flowHandler =
158                    new PinWidgetFlowHandler(providerInfo, mRequest);
159            item = new PendingAddWidgetInfo(providerInfo) {
160                @Override
161                public WidgetAddFlowHandler getHandler() {
162                    return flowHandler;
163                }
164            };
165        }
166        View view = new View(mLauncher);
167        view.setTag(item);
168
169        Point downPos = new Point((int) event.getX(), (int) event.getY());
170        DragOptions options = new DragOptions();
171        options.systemDndStartPoint = downPos;
172        options.preDragCondition = this;
173
174        // We use drag event position as the screenPos for the preview image. Since mPreviewRect
175        // already includes the view position relative to the drag event on the source window,
176        // and the absolute position (position relative to the screen) of drag event is same
177        // across windows, using drag position here give a good estimate for relative position
178        // to source window.
179        PendingItemDragHelper dragHelper = new PendingItemDragHelper(view);
180        if (mRequest.getRequestType() == PinItemRequestCompat.REQUEST_TYPE_APPWIDGET) {
181            dragHelper.setPreview(getPreview(mRequest));
182        }
183
184        dragHelper.startDrag(new Rect(mPreviewRect),
185                mPreviewBitmapWidth, mPreviewViewWidth, downPos,  this, options);
186        mDragStartTime = SystemClock.uptimeMillis();
187        return true;
188    }
189
190    @Override
191    public boolean shouldStartDrag(double distanceDragged) {
192        // Stay in pre-drag mode, if workspace is locked.
193        return !mLauncher.isWorkspaceLocked();
194    }
195
196    @Override
197    public void onPreDragStart(DropTarget.DragObject dragObject) {
198        // The predrag starts when the workspace is not yet loaded. In some cases we set
199        // the dragLayer alpha to 0 to have a nice fade-in animation. But that will prevent the
200        // dragView from being visible. Instead just skip the fade-in animation here.
201        mLauncher.getDragLayer().setAlpha(1);
202
203        dragObject.dragView.setColor(
204                mLauncher.getResources().getColor(R.color.delete_target_hover_tint));
205    }
206
207    @Override
208    public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
209        if (dragStarted) {
210            dragObject.dragView.setColor(0);
211        }
212    }
213
214    @Override
215    public boolean supportsAppInfoDropTarget() {
216        return false;
217    }
218
219    @Override
220    public boolean supportsDeleteDropTarget() {
221        return false;
222    }
223
224    @Override
225    public float getIntrinsicIconScaleFactor() {
226        return 1f;
227    }
228
229    @Override
230    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
231            boolean success) {
232        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
233                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
234            // Exit spring loaded mode if we have not successfully dropped or have not handled the
235            // drop in Workspace
236            mLauncher.exitSpringLoadedDragModeDelayed(true,
237                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
238        }
239
240        if (!success) {
241            d.deferDragViewCleanupPostAnimation = false;
242        }
243        postCleanup();
244    }
245
246    @Override
247    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
248            LauncherLogProto.Target targetParent) {
249        targetParent.containerType = ContainerType.PINITEM;
250    }
251
252    private void postCleanup() {
253        if (mLauncher != null) {
254            // Remove any drag params from the launcher intent since the drag operation is complete.
255            Intent newIntent = new Intent(mLauncher.getIntent());
256            newIntent.removeExtra(EXTRA_PIN_ITEM_DRAG_LISTENER);
257            mLauncher.setIntent(newIntent);
258        }
259
260        new Handler(Looper.getMainLooper()).post(new Runnable() {
261            @Override
262            public void run() {
263                removeListener();
264            }
265        });
266    }
267
268    public void removeListener() {
269        if (mLauncher != null) {
270            mLauncher.getDragLayer().setOnDragListener(null);
271        }
272    }
273
274    public static RemoteViews getPreview(PinItemRequestCompat request) {
275        Bundle extras = request.getExtras();
276        if (extras != null &&
277                extras.get(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW) instanceof RemoteViews) {
278            return (RemoteViews) extras.get(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW);
279        }
280        return null;
281    }
282
283    public static boolean handleDragRequest(Launcher launcher, Intent intent) {
284        if (intent == null || !Intent.ACTION_MAIN.equals(intent.getAction())) {
285            return false;
286        }
287        Parcelable dragExtra = intent.getParcelableExtra(EXTRA_PIN_ITEM_DRAG_LISTENER);
288        if (dragExtra instanceof PinItemDragListener) {
289            PinItemDragListener dragListener = (PinItemDragListener) dragExtra;
290            dragListener.setLauncher(launcher);
291
292            launcher.getDragLayer().setOnDragListener(dragListener);
293            return true;
294        }
295        return false;
296    }
297
298    public static final Parcelable.Creator<PinItemDragListener> CREATOR =
299            new Parcelable.Creator<PinItemDragListener>() {
300                public PinItemDragListener createFromParcel(Parcel source) {
301                    return new PinItemDragListener(source);
302                }
303
304                public PinItemDragListener[] newArray(int size) {
305                    return new PinItemDragListener[size];
306                }
307            };
308}
309