diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index a8d13673e1..75c28f9690 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -299,6 +299,10 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_TRANSIENT_TASKBAR = getDebugFlag( "ENABLE_TRANSIENT_TASKBAR", false, "Enables transient taskbar."); + public static final BooleanFlag SECONDARY_DRAG_N_DROP_TO_PIN = getDebugFlag( + "SECONDARY_DRAG_N_DROP_TO_PIN", false, + "Enable dragging and dropping to pin apps within secondary display"); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { diff --git a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java index a0ed77e38f..f03c62ac5c 100644 --- a/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java +++ b/src/com/android/launcher3/secondarydisplay/PinnedAppsAdapter.java @@ -168,15 +168,18 @@ public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreference mPrefs.unregisterOnSharedPreferenceChangeListener(this); } - private void update(ItemInfo info, Function op) { + /** + * Pins or unpins apps from home screen + */ + public void update(ItemInfo info, Function op) { ComponentKey key = new ComponentKey(info.getTargetComponent(), info.user); if (op.apply(key)) { createFilteredAppsList(); Set copy = new HashSet<>(mPinnedApps); Executors.MODEL_EXECUTOR.submit(() -> mPrefs.edit().putStringSet(PINNED_APPS_KEY, - copy.stream().map(this::encode).collect(Collectors.toSet())) - .apply()); + copy.stream().map(this::encode).collect(Collectors.toSet())) + .apply()); } } @@ -210,6 +213,13 @@ public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreference mPinnedApps.contains(new ComponentKey(info.getTargetComponent(), info.user))); } + /** + * Pins app to home screen + */ + public void addPinnedApp(ItemInfo info) { + update(info, mPinnedApps::add); + } + private class PinUnPinShortcut extends SystemShortcut { private final boolean mIsPinned; diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index a55f7e3b69..1bea5908e8 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -18,6 +18,9 @@ package com.android.launcher3.secondarydisplay; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Intent; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; @@ -26,6 +29,9 @@ import android.view.inputmethod.InputMethodManager; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; @@ -33,6 +39,11 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.dragndrop.DragController; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.dragndrop.DraggableView; +import com.android.launcher3.graphics.DragPreviewProvider; +import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.StringCache; import com.android.launcher3.model.data.AppInfo; @@ -52,11 +63,11 @@ import java.util.HashMap; * Launcher activity for secondary displays */ public class SecondaryDisplayLauncher extends BaseDraggingActivity - implements BgDataModel.Callbacks { + implements BgDataModel.Callbacks, DragController.DragListener { private LauncherModel mModel; - private BaseDragLayer mDragLayer; + private SecondaryDragController mDragController; private ActivityAllAppsContainerView mAppsView; private View mAppsButton; @@ -69,10 +80,13 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity private boolean mBindingItems = false; private SecondaryDisplayPredictions mSecondaryDisplayPredictions; + private final int[] mTempXY = new int[2]; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mModel = LauncherAppState.getInstance(this).getModel(); + mDragController = new SecondaryDragController(this); mOnboardingPrefs = new OnboardingPrefs<>(this, Utilities.getPrefs(this)); mSecondaryDisplayPredictions = SecondaryDisplayPredictions.newInstance(this); if (getWindow().getDecorView().isAttachedToWindow()) { @@ -86,6 +100,12 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity initUi(); } + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.getDragController().removeDragListener(this); + } + private void initUi() { if (mDragLayer != null) { return; @@ -106,12 +126,19 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity mAppsView = findViewById(R.id.apps_view); mAppsButton = findViewById(R.id.all_apps_button); + mDragController.addDragListener(this); mPopupDataProvider = new PopupDataProvider( mAppsView.getAppsStore()::updateNotificationDots); mModel.addCallbacksAndLoad(this); } + @Override + protected void onPause() { + super.onPause(); + mDragController.cancelDrag(); + } + @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -129,12 +156,21 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity showAppDrawer(false); } + public DragController getDragController() { + return mDragController; + } + @Override public void onBackPressed() { if (finishAutoCancelActionMode()) { return; } + if (mDragController.isDragging()) { + mDragController.cancelDrag(); + return; + } + // Note: There should be at most one log per method call. This is enforced implicitly // by using if-else statements. AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(this); @@ -202,7 +238,7 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity float closeR = Themes.getDialogCornerRadius(this); float startR = mAppsButton.getWidth() / 2f; - float[] buttonPos = new float[] { startR, startR}; + float[] buttonPos = new float[]{startR, startR}; mDragLayer.getDescendantCoordRelativeToSelf(mAppsButton, buttonPos); mDragLayer.mapCoordInSelfToDescendant(mAppsView, buttonPos); final Animator animator = ViewAnimationUtils.createCircularReveal(mAppsView, @@ -236,6 +272,7 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity @Override public void startBinding() { mBindingItems = true; + mDragController.cancelDrag(); } @Override @@ -308,4 +345,101 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity startActivitySafely(v, intent, item); } } + + /** + * Core functionality for beginning a drag operation for an item that will be dropped within + * the secondary display grid home screen + */ + public void beginDragShared(View child, DragSource source, DragOptions options) { + Object dragObject = child.getTag(); + if (!(dragObject instanceof ItemInfo)) { + String msg = "Drag started with a view that has no tag set. This " + + "will cause a crash (issue 11627249) down the line. " + + "View: " + child + " tag: " + child.getTag(); + throw new IllegalStateException(msg); + } + beginDragShared(child, source, (ItemInfo) dragObject, + new DragPreviewProvider(child), options); + } + + private void beginDragShared(View child, DragSource source, + ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions options) { + + float iconScale = 1f; + if (child instanceof BubbleTextView) { + FastBitmapDrawable icon = ((BubbleTextView) child).getIcon(); + if (icon != null) { + iconScale = icon.getAnimatedScale(); + } + } + + // clear pressed state if necessary + child.clearFocus(); + child.setPressed(false); + if (child instanceof BubbleTextView) { + BubbleTextView icon = (BubbleTextView) child; + icon.clearPressedBackground(); + } + + DraggableView draggableView = null; + if (child instanceof DraggableView) { + draggableView = (DraggableView) child; + } + + final View contentView = previewProvider.getContentView(); + final float scale; + // The draggable drawable follows the touch point around on the screen + final Drawable drawable; + if (contentView == null) { + drawable = previewProvider.createDrawable(); + scale = previewProvider.getScaleAndPosition(drawable, mTempXY); + } else { + drawable = null; + scale = previewProvider.getScaleAndPosition(contentView, mTempXY); + } + int halfPadding = previewProvider.previewPadding / 2; + int dragLayerX = mTempXY[0]; + int dragLayerY = mTempXY[1]; + + Point dragVisualizeOffset = null; + Rect dragRect = new Rect(); + if (draggableView != null) { + draggableView.getSourceVisualDragBounds(dragRect); + dragLayerY += dragRect.top; + dragVisualizeOffset = new Point(-halfPadding, halfPadding); + } + if (contentView != null) { + mDragController.startDrag( + contentView, + draggableView, + dragLayerX, + dragLayerY, + source, + dragObject, + dragVisualizeOffset, + dragRect, + scale * iconScale, + scale, + options); + } else { + mDragController.startDrag( + drawable, + draggableView, + dragLayerX, + dragLayerY, + source, + dragObject, + dragVisualizeOffset, + dragRect, + scale * iconScale, + scale, + options); + } + } + + @Override + public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { } + + @Override + public void onDragEnd() { } } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java new file mode 100644 index 0000000000..9bf27642ef --- /dev/null +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragController.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 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.secondarydisplay; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.HapticFeedbackConstants; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DragSource; +import com.android.launcher3.DropTarget; +import com.android.launcher3.R; +import com.android.launcher3.accessibility.DragViewStateAnnouncer; +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.model.data.ItemInfo; +import com.android.launcher3.testing.shared.TestProtocol; + +/** + * Drag controller for Secondary Launcher activity + */ +public class SecondaryDragController extends DragController { + + private static final boolean PROFILE_DRAWING_DURING_DRAG = false; + + public SecondaryDragController(SecondaryDisplayLauncher secondaryLauncher) { + super(secondaryLauncher); + } + + @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) { + + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.NO_DROP_TARGET, "5"); + } + + if (PROFILE_DRAWING_DURING_DRAG) { + android.os.Debug.startMethodTracing("Launcher"); + } + mActivity.hideKeyboard(); + + mOptions = options; + if (mOptions.simulatedDndStartPoint != null) { + mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x; + mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y; + } + + final int registrationX = mMotionDown.x - dragLayerX; + final int registrationY = 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; + + mIsInPreDrag = mOptions.preDragCondition != null + && !mOptions.preDragCondition.shouldStartDrag(0); + + final Resources res = mActivity.getResources(); + final float scaleDps = mIsInPreDrag + ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; + + final DragView dragView = mDragObject.dragView = drawable != null + ? new SecondaryDragView( + mActivity, + drawable, + registrationX, + registrationY, + initialDragViewScale, + dragViewScaleOnDrop, + scaleDps) + : new SecondaryDragView( + mActivity, + view, + view.getMeasuredWidth(), + view.getMeasuredHeight(), + registrationX, + registrationY, + initialDragViewScale, + dragViewScaleOnDrop, + scaleDps); + dragView.setItemInfo(dragInfo); + mDragObject.dragComplete = false; + + mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft); + mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop); + + mDragDriver = DragDriver.create(this, mOptions, ev -> { + }); + if (!mOptions.isAccessibleDrag) { + mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); + } + + mDragObject.dragSource = source; + mDragObject.dragInfo = dragInfo; + mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy(); + + if (dragOffset != null) { + dragView.setDragVisualizeOffset(new Point(dragOffset)); + } + if (dragRegion != null) { + dragView.setDragRegion(new Rect(dragRegion)); + } + + mActivity.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + 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 exitDrag() { } + + @Override + protected DropTarget getDefaultDropTarget(int[] dropCoordinates) { + DropTarget target = new DropTarget() { + @Override + public boolean isDropEnabled() { + return true; + } + + @Override + public void onDrop(DragObject dragObject, DragOptions options) { + ((SecondaryDragLayer) mActivity.getDragLayer()).getPinnedAppsAdapter().addPinnedApp( + dragObject.dragInfo); + dragObject.dragView.remove(); + } + + @Override + public void onDragEnter(DragObject dragObject) { + if (getDistanceDragged() > mActivity.getResources().getDimensionPixelSize( + R.dimen.drag_distanceThreshold)) { + mActivity.showAppDrawer(false); + AbstractFloatingView.closeAllOpenViews(mActivity); + } + } + + @Override + public void onDragOver(DragObject dragObject) { } + + @Override + public void onDragExit(DragObject dragObject) { } + + @Override + public boolean acceptDrop(DragObject dragObject) { + return true; + } + + @Override + public void prepareAccessibilityDrop() { } + + @Override + public void getHitRectRelativeToDragLayer(Rect outRect) { } + }; + return target; + } +} diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java index c79d70dacc..42d597044e 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java @@ -29,8 +29,12 @@ import android.widget.GridView; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DropTarget; import com.android.launcher3.R; import com.android.launcher3.allapps.ActivityAllAppsContainerView; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; @@ -59,7 +63,8 @@ public class SecondaryDragLayer extends BaseDragLayer @Override public void recreateControllers() { - mControllers = new TouchController[] {new CloseAllAppsTouchController()}; + mControllers = new TouchController[]{new CloseAllAppsTouchController(), + mActivity.getDragController()}; } /** @@ -166,6 +171,10 @@ public class SecondaryDragLayer extends BaseDragLayer } } + public PinnedAppsAdapter getPinnedAppsAdapter() { + return mPinnedAppsAdapter; + } + private boolean onIconLongClicked(View v) { if (!(v instanceof BubbleTextView)) { return false; @@ -183,6 +192,7 @@ public class SecondaryDragLayer extends BaseDragLayer if (popupDataProvider == null) { return false; } + final PopupContainerWithArrow container = (PopupContainerWithArrow) mActivity.getLayoutInflater().inflate( R.layout.popup_container, mActivity.getDragLayer(), false); @@ -192,7 +202,42 @@ public class SecondaryDragLayer extends BaseDragLayer Collections.emptyList(), Arrays.asList(mPinnedAppsAdapter.getSystemShortcut(item, v), APP_INFO.getShortcut(mActivity, item, v))); - v.getParent().requestDisallowInterceptTouchEvent(true); + container.requestFocus(); + + if (!FeatureFlags.SECONDARY_DRAG_N_DROP_TO_PIN.get() || !mActivity.isAppDrawerShown()) { + return true; + } + + DragOptions options = new DragOptions(); + DeviceProfile grid = mActivity.getDeviceProfile(); + options.intrinsicIconScaleFactor = (float) grid.allAppsIconSizePx / grid.iconSizePx; + options.preDragCondition = container.createPreDragCondition(false); + if (options.preDragCondition == null) { + options.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 (!shouldStartDrag(0)) { + mDragView.setOnAnimationEndCallback(() -> { + mActivity.beginDragShared(v, mActivity.getAppsView(), options); + }); + } + } + + @Override + public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) { + mDragView = null; + } + }; + } + mActivity.beginDragShared(v, mActivity.getAppsView(), options); return true; } } diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragView.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragView.java new file mode 100644 index 0000000000..0168b8fc0d --- /dev/null +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragView.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 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.secondarydisplay; + +import android.graphics.drawable.Drawable; +import android.view.View; + +import com.android.launcher3.R; +import com.android.launcher3.dragndrop.DragView; + +/** + * A DragView drawn/used by the Secondary Launcher activity. + */ +public class SecondaryDragView extends DragView { + + public SecondaryDragView(SecondaryDisplayLauncher launcher, + Drawable drawable, + int registrationX, int registrationY, float initialScale, float scaleOnDrop, + float finalScaleDps) { + super(launcher, drawable, registrationX, registrationY, initialScale, scaleOnDrop, + finalScaleDps); + } + + public SecondaryDragView(SecondaryDisplayLauncher launcher, View content, int width, int height, + int registrationX, int registrationY, float initialScale, float scaleOnDrop, + float finalScaleDps) { + super(launcher, content, width, height, registrationX, registrationY, initialScale, + scaleOnDrop, finalScaleDps); + } + + @Override + public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) { + Runnable onAnimationEnd = () -> { + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + mActivity.getDragLayer().removeView(this); + }; + + duration = Math.max(duration, + getResources().getInteger(R.integer.config_dropAnimMinDuration)); + + animate() + .translationX(toTouchX - mRegistrationX) + .translationY(toTouchY - mRegistrationY) + .scaleX(mScaleOnDrop) + .scaleY(mScaleOnDrop) + .withEndAction(onAnimationEnd) + .setDuration(duration) + .start(); + } +}