1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.taskbar;
17 
18 import android.content.ClipData;
19 import android.content.ClipDescription;
20 import android.content.Intent;
21 import android.content.pm.LauncherApps;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.Point;
25 import android.graphics.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.os.UserHandle;
28 import android.view.DragEvent;
29 import android.view.MotionEvent;
30 import android.view.View;
31 
32 import androidx.annotation.Nullable;
33 
34 import com.android.internal.logging.InstanceId;
35 import com.android.internal.logging.InstanceIdSequence;
36 import com.android.launcher3.AbstractFloatingView;
37 import com.android.launcher3.BubbleTextView;
38 import com.android.launcher3.DragSource;
39 import com.android.launcher3.DropTarget;
40 import com.android.launcher3.LauncherSettings;
41 import com.android.launcher3.R;
42 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
43 import com.android.launcher3.config.FeatureFlags;
44 import com.android.launcher3.dragndrop.DragController;
45 import com.android.launcher3.dragndrop.DragDriver;
46 import com.android.launcher3.dragndrop.DragOptions;
47 import com.android.launcher3.dragndrop.DragView;
48 import com.android.launcher3.dragndrop.DraggableView;
49 import com.android.launcher3.graphics.DragPreviewProvider;
50 import com.android.launcher3.logging.StatsLogManager;
51 import com.android.launcher3.model.data.ItemInfo;
52 import com.android.launcher3.model.data.WorkspaceItemInfo;
53 import com.android.launcher3.popup.PopupContainerWithArrow;
54 import com.android.systemui.shared.recents.model.Task;
55 import com.android.systemui.shared.system.ClipDescriptionCompat;
56 import com.android.systemui.shared.system.LauncherAppsCompat;
57 
58 /**
59  * Handles long click on Taskbar items to start a system drag and drop operation.
60  */
61 public class TaskbarDragController extends DragController<TaskbarActivityContext>  {
62 
63     private final int mDragIconSize;
64     private final int[] mTempXY = new int[2];
65 
66     // Initialized in init.
67     TaskbarControllers mControllers;
68 
69     // Where the initial touch was relative to the dragged icon.
70     private int mRegistrationX;
71     private int mRegistrationY;
72 
73     private boolean mIsSystemDragInProgress;
74 
TaskbarDragController(TaskbarActivityContext activity)75     public TaskbarDragController(TaskbarActivityContext activity) {
76         super(activity);
77         Resources resources = mActivity.getResources();
78         mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size);
79     }
80 
init(TaskbarControllers controllers)81     public void init(TaskbarControllers controllers) {
82         mControllers = controllers;
83     }
84 
85     /**
86      * Attempts to start a system drag and drop operation for the given View, using its tag to
87      * generate the ClipDescription and Intent.
88      * @return Whether {@link View#startDragAndDrop} started successfully.
89      */
startDragOnLongClick(View view)90     protected boolean startDragOnLongClick(View view) {
91         if (!(view instanceof BubbleTextView)) {
92             return false;
93         }
94 
95         BubbleTextView btv = (BubbleTextView) view;
96 
97         mActivity.setTaskbarWindowFullscreen(true);
98         btv.post(() -> {
99             startInternalDrag(btv);
100             btv.getIcon().setIsDisabled(true);
101             mControllers.taskbarAutohideSuspendController.updateFlag(
102                     TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, true);
103         });
104         return true;
105     }
106 
startInternalDrag(BubbleTextView btv)107     private void startInternalDrag(BubbleTextView btv) {
108         float iconScale = btv.getIcon().getAnimatedScale();
109 
110         // Clear the pressed state if necessary
111         btv.clearFocus();
112         btv.setPressed(false);
113         btv.clearPressedBackground();
114 
115         final DragPreviewProvider previewProvider = new DragPreviewProvider(btv);
116         final Drawable drawable = previewProvider.createDrawable();
117         final float scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
118         int dragLayerX = mTempXY[0];
119         int dragLayerY = mTempXY[1];
120 
121         Rect dragRect = new Rect();
122         btv.getSourceVisualDragBounds(dragRect);
123         dragLayerY += dragRect.top;
124 
125         DragOptions dragOptions = new DragOptions();
126         dragOptions.preDragCondition = new DragOptions.PreDragCondition() {
127             private DragView mDragView;
128 
129             @Override
130             public boolean shouldStartDrag(double distanceDragged) {
131                 return mDragView != null && mDragView.isAnimationFinished();
132             }
133 
134             @Override
135             public void onPreDragStart(DropTarget.DragObject dragObject) {
136                 mDragView = dragObject.dragView;
137             }
138 
139             @Override
140             public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
141                 mDragView = null;
142             }
143         };
144         if (FeatureFlags.ENABLE_TASKBAR_POPUP_MENU.get()) {
145             PopupContainerWithArrow<TaskbarActivityContext> popupContainer =
146                     mControllers.taskbarPopupController.showForIcon(btv);
147             if (popupContainer != null) {
148                 dragOptions.preDragCondition = popupContainer.createPreDragCondition();
149             }
150         }
151 
152         startDrag(
153                 drawable,
154                 /* view = */ null,
155                 /* originalView = */ btv,
156                 dragLayerX,
157                 dragLayerY,
158                 (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
159                 (WorkspaceItemInfo) btv.getTag(),
160                 /* dragVisualizeOffset = */ null,
161                 dragRect,
162                 scale * iconScale,
163                 scale,
164                 dragOptions);
165     }
166 
167     @Override
startDrag(@ullable Drawable drawable, @Nullable View view, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)168     protected DragView startDrag(@Nullable Drawable drawable, @Nullable View view,
169             DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source,
170             ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale,
171             float dragViewScaleOnDrop, DragOptions options) {
172         mOptions = options;
173 
174         mRegistrationX = mMotionDown.x - dragLayerX;
175         mRegistrationY = mMotionDown.y - dragLayerY;
176 
177         final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
178         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
179 
180         mLastDropTarget = null;
181 
182         mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext());
183         mDragObject.originalView = originalView;
184         mDragObject.deferDragViewCleanupPostAnimation = false;
185 
186         mIsInPreDrag = mOptions.preDragCondition != null
187                 && !mOptions.preDragCondition.shouldStartDrag(0);
188 
189         float scalePx = mDragIconSize - dragRegion.width();
190         final DragView dragView = mDragObject.dragView = new TaskbarDragView(
191                 mActivity,
192                 drawable,
193                 mRegistrationX,
194                 mRegistrationY,
195                 initialDragViewScale,
196                 dragViewScaleOnDrop,
197                 scalePx);
198         dragView.setItemInfo(dragInfo);
199         mDragObject.dragComplete = false;
200 
201         mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
202         mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
203 
204         mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {});
205         if (!mOptions.isAccessibleDrag) {
206             mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
207         }
208 
209         mDragObject.dragSource = source;
210         mDragObject.dragInfo = dragInfo;
211         mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
212 
213         if (dragRegion != null) {
214             dragView.setDragRegion(new Rect(dragRegion));
215         }
216 
217         dragView.show(mLastTouch.x, mLastTouch.y);
218         mDistanceSinceScroll = 0;
219 
220         if (!mIsInPreDrag) {
221             callOnDragStart();
222         } else if (mOptions.preDragCondition != null) {
223             mOptions.preDragCondition.onPreDragStart(mDragObject);
224         }
225 
226         handleMoveEvent(mLastTouch.x, mLastTouch.y);
227 
228         return dragView;
229     }
230 
231     @Override
callOnDragStart()232     protected void callOnDragStart() {
233         super.callOnDragStart();
234         // Pre-drag has ended, start the global system drag.
235         AbstractFloatingView.closeAllOpenViews(mActivity);
236         startSystemDrag((BubbleTextView) mDragObject.originalView);
237     }
238 
startSystemDrag(BubbleTextView btv)239     private void startSystemDrag(BubbleTextView btv) {
240         View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) {
241 
242             @Override
243             public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
244                 shadowSize.set(mDragIconSize, mDragIconSize);
245                 // The registration point was taken before the icon scaled to mDragIconSize, so
246                 // offset the registration to where the touch is on the new size.
247                 int offsetX = (mDragIconSize - mDragObject.dragView.getDragRegionWidth()) / 2;
248                 int offsetY = (mDragIconSize - mDragObject.dragView.getDragRegionHeight()) / 2;
249                 shadowTouchPoint.set(mRegistrationX + offsetX, mRegistrationY + offsetY);
250             }
251 
252             @Override
253             public void onDrawShadow(Canvas canvas) {
254                 canvas.save();
255                 float scale = mDragObject.dragView.getScaleX();
256                 canvas.scale(scale, scale);
257                 mDragObject.dragView.draw(canvas);
258                 canvas.restore();
259             }
260         };
261 
262         Object tag = btv.getTag();
263         ClipDescription clipDescription = null;
264         Intent intent = null;
265         if (tag instanceof WorkspaceItemInfo) {
266             WorkspaceItemInfo item = (WorkspaceItemInfo) tag;
267             LauncherApps launcherApps = mActivity.getSystemService(LauncherApps.class);
268             clipDescription = new ClipDescription(item.title,
269                     new String[] {
270                             item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
271                                     ? ClipDescriptionCompat.MIMETYPE_APPLICATION_SHORTCUT
272                                     : ClipDescriptionCompat.MIMETYPE_APPLICATION_ACTIVITY
273                     });
274             intent = new Intent();
275             if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
276                 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
277                 intent.putExtra(Intent.EXTRA_SHORTCUT_ID, item.getDeepShortcutId());
278             } else {
279                 intent.putExtra(ClipDescriptionCompat.EXTRA_PENDING_INTENT,
280                         LauncherAppsCompat.getMainActivityLaunchIntent(launcherApps,
281                                 item.getIntent().getComponent(), null, item.user));
282             }
283             intent.putExtra(Intent.EXTRA_USER, item.user);
284         } else if (tag instanceof Task) {
285             Task task = (Task) tag;
286             clipDescription = new ClipDescription(task.titleDescription,
287                     new String[] {
288                             ClipDescriptionCompat.MIMETYPE_APPLICATION_TASK
289                     });
290             intent = new Intent();
291             intent.putExtra(ClipDescriptionCompat.EXTRA_TASK_ID, task.key.id);
292             intent.putExtra(Intent.EXTRA_USER, UserHandle.of(task.key.userId));
293         }
294 
295         if (clipDescription != null && intent != null) {
296             // Need to share the same InstanceId between launcher3 and WM Shell (internal).
297             InstanceId internalInstanceId = new InstanceIdSequence(
298                     com.android.launcher3.logging.InstanceId.INSTANCE_ID_MAX).newInstanceId();
299             com.android.launcher3.logging.InstanceId launcherInstanceId =
300                     new com.android.launcher3.logging.InstanceId(internalInstanceId.getId());
301 
302             intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId);
303 
304             ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent));
305             if (btv.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
306                     View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE)) {
307                 onSystemDragStarted();
308 
309                 mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
310                         .withInstanceId(launcherInstanceId)
311                         .log(StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
312             }
313         }
314     }
315 
onSystemDragStarted()316     private void onSystemDragStarted() {
317         mIsSystemDragInProgress = true;
318         mActivity.getDragLayer().setOnDragListener((view, dragEvent) -> {
319             switch (dragEvent.getAction()) {
320                 case DragEvent.ACTION_DRAG_STARTED:
321                     // Return true to tell system we are interested in events, so we get DRAG_ENDED.
322                     return true;
323                 case DragEvent.ACTION_DRAG_ENDED:
324                     mIsSystemDragInProgress = false;
325                     maybeOnDragEnd();
326                     return true;
327             }
328             return false;
329         });
330     }
331 
332     @Override
isDragging()333     public boolean isDragging() {
334         return super.isDragging() || mIsSystemDragInProgress;
335     }
336 
maybeOnDragEnd()337     private void maybeOnDragEnd() {
338         if (!isDragging()) {
339             ((BubbleTextView) mDragObject.originalView).getIcon().setIsDisabled(false);
340             mControllers.taskbarAutohideSuspendController.updateFlag(
341                     TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false);
342         }
343     }
344 
345     @Override
callOnDragEnd()346     protected void callOnDragEnd() {
347         super.callOnDragEnd();
348         maybeOnDragEnd();
349     }
350 
351     @Override
getX(MotionEvent ev)352     protected float getX(MotionEvent ev) {
353         // We will resize to fill the screen while dragging, so use screen coordinates. This ensures
354         // we start at the correct position even though touch down is on the smaller DragLayer size.
355         return ev.getRawX();
356     }
357 
358     @Override
getY(MotionEvent ev)359     protected float getY(MotionEvent ev) {
360         // We will resize to fill the screen while dragging, so use screen coordinates. This ensures
361         // we start at the correct position even though touch down is on the smaller DragLayer size.
362         return ev.getRawY();
363     }
364 
365     @Override
getClampedDragLayerPos(float x, float y)366     protected Point getClampedDragLayerPos(float x, float y) {
367         // No need to clamp, as we will take up the entire screen.
368         mTmpPoint.set(Math.round(x), Math.round(y));
369         return mTmpPoint;
370     }
371 
372     @Override
exitDrag()373     protected void exitDrag() {
374         if (mDragObject != null) {
375             mActivity.getDragLayer().removeView(mDragObject.dragView);
376         }
377     }
378 
379     @Override
addDropTarget(DropTarget target)380     public void addDropTarget(DropTarget target) {
381         // No-op as Taskbar currently doesn't support any drop targets internally.
382         // Note: if we do add internal DropTargets, we'll still need to ignore Folder.
383     }
384 
385     @Override
getDefaultDropTarget(int[] dropCoordinates)386     protected DropTarget getDefaultDropTarget(int[] dropCoordinates) {
387         return null;
388     }
389 }
390