| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.launcher3.taskbar; |
| |
| import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; |
| import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; |
| import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.ClipData; |
| import android.content.ClipDescription; |
| import android.content.Intent; |
| import android.content.pm.LauncherApps; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.UserHandle; |
| import android.util.Pair; |
| import android.view.DragEvent; |
| import android.view.MotionEvent; |
| import android.view.SurfaceControl; |
| import android.view.View; |
| import android.view.ViewRootImpl; |
| import android.window.SurfaceSyncer; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.logging.InstanceId; |
| import com.android.launcher3.AbstractFloatingView; |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.DragSource; |
| import com.android.launcher3.DropTarget; |
| import com.android.launcher3.LauncherSettings; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.accessibility.DragViewStateAnnouncer; |
| import com.android.launcher3.anim.Interpolators; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.dragndrop.DragController; |
| import com.android.launcher3.dragndrop.DragDriver; |
| import com.android.launcher3.dragndrop.DragOptions; |
| import com.android.launcher3.dragndrop.DragView; |
| import com.android.launcher3.dragndrop.DraggableView; |
| import com.android.launcher3.graphics.DragPreviewProvider; |
| import com.android.launcher3.logging.StatsLogManager; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.model.data.WorkspaceItemInfo; |
| import com.android.launcher3.popup.PopupContainerWithArrow; |
| import com.android.launcher3.shortcuts.DeepShortcutView; |
| import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; |
| import com.android.launcher3.testing.TestLogging; |
| import com.android.launcher3.testing.shared.TestProtocol; |
| import com.android.launcher3.util.IntSet; |
| import com.android.launcher3.util.ItemInfoMatcher; |
| import com.android.quickstep.util.LogUtils; |
| import com.android.systemui.shared.recents.model.Task; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.function.Predicate; |
| |
| /** |
| * Handles long click on Taskbar items to start a system drag and drop operation. |
| */ |
| public class TaskbarDragController extends DragController<BaseTaskbarContext> implements |
| TaskbarControllers.LoggableTaskbarController { |
| |
| private static boolean DEBUG_DRAG_SHADOW_SURFACE = false; |
| |
| private final int mDragIconSize; |
| private final int[] mTempXY = new int[2]; |
| |
| // Initialized in init. |
| TaskbarControllers mControllers; |
| |
| // Where the initial touch was relative to the dragged icon. |
| private int mRegistrationX; |
| private int mRegistrationY; |
| |
| private boolean mIsSystemDragInProgress; |
| |
| // Animation for the drag shadow back into position after an unsuccessful drag |
| private ValueAnimator mReturnAnimator; |
| |
| public TaskbarDragController(BaseTaskbarContext activity) { |
| super(activity); |
| Resources resources = mActivity.getResources(); |
| mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size); |
| } |
| |
| public void init(TaskbarControllers controllers) { |
| mControllers = controllers; |
| } |
| |
| /** |
| * Attempts to start a system drag and drop operation for the given View, using its tag to |
| * generate the ClipDescription and Intent. |
| * @return Whether {@link View#startDragAndDrop} started successfully. |
| */ |
| public boolean startDragOnLongClick(View view) { |
| return startDragOnLongClick(view, null, null); |
| } |
| |
| protected boolean startDragOnLongClick( |
| DeepShortcutView shortcutView, Point iconShift) { |
| return startDragOnLongClick( |
| shortcutView.getBubbleText(), |
| new ShortcutDragPreviewProvider(shortcutView.getIconView(), iconShift), |
| iconShift); |
| } |
| |
| private boolean startDragOnLongClick( |
| View view, |
| @Nullable DragPreviewProvider dragPreviewProvider, |
| @Nullable Point iconShift) { |
| if (!(view instanceof BubbleTextView)) { |
| return false; |
| } |
| TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onTaskbarItemLongClick"); |
| BubbleTextView btv = (BubbleTextView) view; |
| mActivity.onDragStart(); |
| btv.post(() -> { |
| DragView dragView = startInternalDrag(btv, dragPreviewProvider); |
| if (iconShift != null) { |
| dragView.animateShift(-iconShift.x, -iconShift.y); |
| } |
| btv.getIcon().setIsDisabled(true); |
| mControllers.taskbarAutohideSuspendController.updateFlag( |
| TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, true); |
| }); |
| return true; |
| } |
| |
| private DragView startInternalDrag( |
| BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) { |
| float iconScale = btv.getIcon().getAnimatedScale(); |
| |
| // Clear the pressed state if necessary |
| btv.clearFocus(); |
| btv.setPressed(false); |
| btv.clearPressedBackground(); |
| |
| final DragPreviewProvider previewProvider = dragPreviewProvider == null |
| ? new DragPreviewProvider(btv) : dragPreviewProvider; |
| final Drawable drawable = previewProvider.createDrawable(); |
| final float scale = previewProvider.getScaleAndPosition(drawable, mTempXY); |
| int dragLayerX = mTempXY[0]; |
| int dragLayerY = mTempXY[1]; |
| |
| Rect dragRect = new Rect(); |
| btv.getSourceVisualDragBounds(dragRect); |
| dragLayerY += dragRect.top; |
| |
| DragOptions dragOptions = new DragOptions(); |
| dragOptions.preDragCondition = null; |
| if (FeatureFlags.ENABLE_TASKBAR_POPUP_MENU.get()) { |
| PopupContainerWithArrow<BaseTaskbarContext> popupContainer = |
| mControllers.taskbarPopupController.showForIcon(btv); |
| if (popupContainer != null) { |
| dragOptions.preDragCondition = popupContainer.createPreDragCondition(false); |
| } |
| } |
| if (dragOptions.preDragCondition == null) { |
| dragOptions.preDragCondition = new DragOptions.PreDragCondition() { |
| private DragView mDragView; |
| |
| @Override |
| public boolean shouldStartDrag(double distanceDragged) { |
| return mDragView != null && mDragView.isAnimationFinished(); |
| } |
| |
| @Override |
| public void onPreDragStart(DropTarget.DragObject dragObject) { |
| mDragView = dragObject.dragView; |
| |
| if (FeatureFlags.ENABLE_TASKBAR_POPUP_MENU.get() |
| && !shouldStartDrag(0)) { |
| // Immediately close the popup menu. |
| mDragView.setOnAnimationEndCallback(() -> callOnDragStart()); |
| } |
| } |
| |
| @Override |
| public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { |
| mDragView = null; |
| } |
| }; |
| } |
| |
| return startDrag( |
| drawable, |
| /* view = */ null, |
| /* originalView = */ btv, |
| dragLayerX, |
| dragLayerY, |
| (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */, |
| (ItemInfo) btv.getTag(), |
| /* dragVisualizeOffset = */ null, |
| dragRect, |
| scale * iconScale, |
| scale, |
| dragOptions); |
| } |
| |
| @Override |
| protected DragView startDrag(@Nullable 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) { |
| mOptions = options; |
| |
| mRegistrationX = mMotionDown.x - dragLayerX; |
| mRegistrationY = mMotionDown.y - dragLayerY; |
| |
| final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; |
| final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; |
| |
| mLastDropTarget = null; |
| |
| mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext()); |
| mDragObject.originalView = originalView; |
| mDragObject.deferDragViewCleanupPostAnimation = false; |
| |
| mIsInPreDrag = mOptions.preDragCondition != null |
| && !mOptions.preDragCondition.shouldStartDrag(0); |
| |
| float scalePx = mDragIconSize - dragRegion.width(); |
| final DragView dragView = mDragObject.dragView = new TaskbarDragView( |
| mActivity, |
| drawable, |
| mRegistrationX, |
| mRegistrationY, |
| initialDragViewScale, |
| dragViewScaleOnDrop, |
| scalePx); |
| dragView.setItemInfo(dragInfo); |
| mDragObject.dragComplete = false; |
| |
| mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft); |
| mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop); |
| |
| mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {}); |
| if (!mOptions.isAccessibleDrag) { |
| mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); |
| } |
| |
| mDragObject.dragSource = source; |
| mDragObject.dragInfo = dragInfo; |
| mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy(); |
| |
| if (dragRegion != null) { |
| dragView.setDragRegion(new Rect(dragRegion)); |
| } |
| |
| dragView.show(mLastTouch.x, mLastTouch.y); |
| mDistanceSinceScroll = 0; |
| |
| if (!mIsInPreDrag) { |
| callOnDragStart(); |
| } else if (mOptions.preDragCondition != null) { |
| mOptions.preDragCondition.onPreDragStart(mDragObject); |
| } |
| |
| handleMoveEvent(mLastTouch.x, mLastTouch.y); |
| |
| return dragView; |
| } |
| |
| @Override |
| protected void callOnDragStart() { |
| super.callOnDragStart(); |
| // Pre-drag has ended, start the global system drag. |
| AbstractFloatingView.closeAllOpenViews(mActivity); |
| startSystemDrag((BubbleTextView) mDragObject.originalView); |
| } |
| |
| private void startSystemDrag(BubbleTextView btv) { |
| View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) { |
| |
| @Override |
| public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) { |
| int iconSize = Math.max(mDragIconSize, btv.getWidth()); |
| shadowSize.set(iconSize, iconSize); |
| // The registration point was taken before the icon scaled to mDragIconSize, so |
| // offset the registration to where the touch is on the new size. |
| int offsetX = (mDragIconSize - mDragObject.dragView.getDragRegionWidth()) / 2; |
| int offsetY = (mDragIconSize - mDragObject.dragView.getDragRegionHeight()) / 2; |
| shadowTouchPoint.set(mRegistrationX + offsetX, mRegistrationY + offsetY); |
| } |
| |
| @Override |
| public void onDrawShadow(Canvas canvas) { |
| canvas.save(); |
| if (DEBUG_DRAG_SHADOW_SURFACE) { |
| canvas.drawColor(0xffff0000); |
| } |
| float scale = mDragObject.dragView.getScaleX(); |
| canvas.scale(scale, scale); |
| mDragObject.dragView.draw(canvas); |
| canvas.restore(); |
| } |
| }; |
| |
| Object tag = btv.getTag(); |
| ClipDescription clipDescription = null; |
| Intent intent = null; |
| if (tag instanceof ItemInfo) { |
| ItemInfo item = (ItemInfo) tag; |
| LauncherApps launcherApps = mActivity.getSystemService(LauncherApps.class); |
| clipDescription = new ClipDescription(item.title, |
| new String[] { |
| item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT |
| ? ClipDescription.MIMETYPE_APPLICATION_SHORTCUT |
| : ClipDescription.MIMETYPE_APPLICATION_ACTIVITY |
| }); |
| intent = new Intent(); |
| if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { |
| String deepShortcutId = ((WorkspaceItemInfo) item).getDeepShortcutId(); |
| intent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, |
| launcherApps.getShortcutIntent( |
| item.getIntent().getPackage(), |
| deepShortcutId, |
| null, |
| item.user)); |
| intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage()); |
| intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId); |
| } else { |
| intent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, |
| launcherApps.getMainActivityLaunchIntent(item.getIntent().getComponent(), |
| null, item.user)); |
| } |
| intent.putExtra(Intent.EXTRA_USER, item.user); |
| } else if (tag instanceof Task) { |
| Task task = (Task) tag; |
| clipDescription = new ClipDescription(task.titleDescription, |
| new String[] { |
| ClipDescription.MIMETYPE_APPLICATION_TASK |
| }); |
| intent = new Intent(); |
| intent.putExtra(Intent.EXTRA_TASK_ID, task.key.id); |
| intent.putExtra(Intent.EXTRA_USER, UserHandle.of(task.key.userId)); |
| } |
| |
| if (clipDescription != null && intent != null) { |
| Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = |
| LogUtils.getShellShareableInstanceId(); |
| // Need to share the same InstanceId between launcher3 and WM Shell (internal). |
| InstanceId internalInstanceId = instanceIds.first; |
| com.android.launcher3.logging.InstanceId launcherInstanceId = instanceIds.second; |
| |
| intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId); |
| |
| ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent)); |
| if (btv.startDragAndDrop(clipData, shadowBuilder, null /* localState */, |
| View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE |
| | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION)) { |
| onSystemDragStarted(btv); |
| |
| mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo) |
| .withInstanceId(launcherInstanceId) |
| .log(StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED); |
| } |
| } |
| } |
| |
| private void onSystemDragStarted(BubbleTextView btv) { |
| mIsSystemDragInProgress = true; |
| mActivity.getDragLayer().setOnDragListener((view, dragEvent) -> { |
| switch (dragEvent.getAction()) { |
| case DragEvent.ACTION_DRAG_STARTED: |
| // Return true to tell system we are interested in events, so we get DRAG_ENDED. |
| return true; |
| case DragEvent.ACTION_DRAG_ENDED: |
| mIsSystemDragInProgress = false; |
| if (dragEvent.getResult()) { |
| maybeOnDragEnd(); |
| } else { |
| // This will take care of calling maybeOnDragEnd() after the animation |
| animateGlobalDragViewToOriginalPosition(btv, dragEvent); |
| } |
| return true; |
| } |
| return false; |
| }); |
| } |
| |
| @Override |
| public boolean isDragging() { |
| return super.isDragging() || mIsSystemDragInProgress; |
| } |
| |
| /** {@code true} if the system is currently handling the drag. */ |
| public boolean isSystemDragInProgress() { |
| return mIsSystemDragInProgress; |
| } |
| |
| private void maybeOnDragEnd() { |
| if (!isDragging()) { |
| ((BubbleTextView) mDragObject.originalView).getIcon().setIsDisabled(false); |
| mControllers.taskbarAutohideSuspendController.updateFlag( |
| TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false); |
| mActivity.onDragEnd(); |
| } |
| } |
| |
| @Override |
| protected void callOnDragEnd() { |
| super.callOnDragEnd(); |
| maybeOnDragEnd(); |
| } |
| |
| private void animateGlobalDragViewToOriginalPosition(BubbleTextView btv, |
| DragEvent dragEvent) { |
| SurfaceControl dragSurface = dragEvent.getDragSurface(); |
| |
| // For top level icons, the target is the icon itself |
| View target = btv; |
| Object tag = btv.getTag(); |
| if (tag instanceof ItemInfo) { |
| ItemInfo item = (ItemInfo) tag; |
| TaskbarViewController taskbarViewController = mControllers.taskbarViewController; |
| if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) { |
| // Since all apps closes when the drag starts, target the all apps button instead. |
| target = taskbarViewController.getAllAppsButtonView(); |
| } else if (item.container >= 0) { |
| // Since folders close when the drag starts, target the folder icon instead. |
| Predicate<ItemInfo> matcher = ItemInfoMatcher.forFolderMatch( |
| ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id))); |
| target = taskbarViewController.getFirstIconMatch(matcher); |
| } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) { |
| // Find first icon with same package/user as the deep shortcut. |
| Predicate<ItemInfo> packageUserMatcher = ItemInfoMatcher.ofPackages( |
| Collections.singleton(item.getTargetPackage()), item.user); |
| target = taskbarViewController.getFirstIconMatch(packageUserMatcher); |
| } |
| } |
| |
| // Finish any pending return animation before starting a new drag |
| if (mReturnAnimator != null) { |
| mReturnAnimator.end(); |
| } |
| |
| float fromX = dragEvent.getX() - dragEvent.getOffsetX(); |
| float fromY = dragEvent.getY() - dragEvent.getOffsetY(); |
| int[] toPosition = target.getLocationOnScreen(); |
| float toScale = (float) target.getWidth() / mDragIconSize; |
| float toAlpha = (target == btv) ? 1f : 0f; |
| final ViewRootImpl viewRoot = target.getViewRootImpl(); |
| SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); |
| mReturnAnimator = ValueAnimator.ofFloat(0f, 1f); |
| mReturnAnimator.setDuration(300); |
| mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); |
| mReturnAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| float t = animation.getAnimatedFraction(); |
| float accelT = Interpolators.ACCEL_2.getInterpolation(t); |
| float scale = 1f - t * (1f - toScale); |
| float alpha = 1f - accelT * (1f - toAlpha); |
| tx.setPosition(dragSurface, Utilities.mapRange(t, fromX, toPosition[0]), |
| Utilities.mapRange(t, fromY, toPosition[1])); |
| tx.setScale(dragSurface, scale, scale); |
| tx.setAlpha(dragSurface, alpha); |
| tx.apply(); |
| } |
| }); |
| mReturnAnimator.addListener(new AnimatorListenerAdapter() { |
| private boolean mCanceled = false; |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| cleanUpSurface(); |
| mCanceled = true; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (mCanceled) { |
| return; |
| } |
| cleanUpSurface(); |
| } |
| |
| private void cleanUpSurface() { |
| tx.close(); |
| maybeOnDragEnd(); |
| // Synchronize removing the drag surface with the next draw after calling |
| // maybeOnDragEnd() |
| SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); |
| transaction.remove(dragSurface); |
| SurfaceSyncer syncer = new SurfaceSyncer(); |
| int syncId = syncer.setupSync(transaction::close); |
| syncer.addToSync(syncId, viewRoot.getView()); |
| syncer.addTransactionToSync(syncId, transaction); |
| syncer.markSyncReady(syncId); |
| mReturnAnimator = null; |
| } |
| }); |
| mReturnAnimator.start(); |
| } |
| |
| @Override |
| protected float getX(MotionEvent ev) { |
| // We will resize to fill the screen while dragging, so use screen coordinates. This ensures |
| // we start at the correct position even though touch down is on the smaller DragLayer size. |
| return ev.getRawX(); |
| } |
| |
| @Override |
| protected float getY(MotionEvent ev) { |
| // We will resize to fill the screen while dragging, so use screen coordinates. This ensures |
| // we start at the correct position even though touch down is on the smaller DragLayer size. |
| return ev.getRawY(); |
| } |
| |
| @Override |
| protected Point getClampedDragLayerPos(float x, float y) { |
| // No need to clamp, as we will take up the entire screen. |
| mTmpPoint.set(Math.round(x), Math.round(y)); |
| return mTmpPoint; |
| } |
| |
| @Override |
| protected void exitDrag() { |
| if (mDragObject != null) { |
| mActivity.getDragLayer().removeView(mDragObject.dragView); |
| } |
| } |
| |
| @Override |
| public void addDropTarget(DropTarget target) { |
| // No-op as Taskbar currently doesn't support any drop targets internally. |
| // Note: if we do add internal DropTargets, we'll still need to ignore Folder. |
| } |
| |
| @Override |
| protected DropTarget getDefaultDropTarget(int[] dropCoordinates) { |
| return null; |
| } |
| |
| @Override |
| public void dumpLogs(String prefix, PrintWriter pw) { |
| pw.println(prefix + "TaskbarDragController:"); |
| |
| pw.println(prefix + "\tmDragIconSize=" + mDragIconSize); |
| pw.println(prefix + "\tmTempXY=" + Arrays.toString(mTempXY)); |
| pw.println(prefix + "\tmRegistrationX=" + mRegistrationX); |
| pw.println(prefix + "\tmRegistrationY=" + mRegistrationY); |
| pw.println(prefix + "\tmIsSystemDragInProgress=" + mIsSystemDragInProgress); |
| pw.println(prefix + "\tisInternalDragInProgess=" + super.isDragging()); |
| } |
| } |