diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index e653283674..fc7f614265 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -18,8 +18,6 @@ package com.android.launcher3; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static com.android.launcher3.LauncherState.NORMAL; - import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; @@ -34,12 +32,15 @@ import android.view.accessibility.AccessibilityEvent; import android.widget.PopupWindow; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.launcher3.anim.Interpolators; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.dragndrop.DragView; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.views.ActivityContext; /** * Implements a DropTarget. @@ -59,8 +60,8 @@ public abstract class ButtonDropTarget extends TextView private final Rect mTempRect = new Rect(); - protected final Launcher mLauncher; - + protected final ActivityContext mActivityContext; + protected final DropTargetHandler mDropTargetHandler; protected DropTargetBar mDropTargetBar; /** Whether this drop target is active for the current drag */ @@ -83,13 +84,17 @@ public abstract class ButtonDropTarget extends TextView private PopupWindow mToolTip; private int mToolTipLocation; + public ButtonDropTarget(Context context) { + this(context, null, 0); + } public ButtonDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mLauncher = Launcher.getLauncher(context); + mActivityContext = ActivityContext.lookupContext(context); + mDropTargetHandler = mActivityContext.getDropTargetHandler(); Resources resources = getResources(); mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold); @@ -210,7 +215,8 @@ public abstract class ButtonDropTarget extends TextView @Override public boolean isDropEnabled() { return mActive && (mAccessibleDrag || - mLauncher.getDragController().getDistanceDragged() >= mDragDistanceThreshold); + mActivityContext.getDragController().getDistanceDragged() + >= mDragDistanceThreshold); } @Override @@ -229,7 +235,8 @@ public abstract class ButtonDropTarget extends TextView // FlingAnimation handles the animation and then calls completeDrop(). return; } - final DragLayer dragLayer = mLauncher.getDragLayer(); + + final DragLayer dragLayer = mDropTargetHandler.getDragLayer(); final DragView dragView = d.dragView; final Rect to = getIconRect(d); final float scale = (float) to.width() / dragView.getMeasuredWidth(); @@ -240,9 +247,10 @@ public abstract class ButtonDropTarget extends TextView Runnable onAnimationEndRunnable = () -> { completeDrop(d); mDropTargetBar.onDragEnd(); - mLauncher.getStateManager().goToState(NORMAL); + mDropTargetHandler.onDropAnimationComplete(); }; + dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f, DRAG_VIEW_DROP_DURATION, Interpolators.DEACCEL_2, onAnimationEndRunnable, @@ -261,10 +269,10 @@ public abstract class ButtonDropTarget extends TextView @Override public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { super.getHitRect(outRect); - outRect.bottom += mLauncher.getDeviceProfile().dropTargetDragPaddingPx; + outRect.bottom += mActivityContext.getDeviceProfile().dropTargetDragPaddingPx; sTempCords[0] = sTempCords[1] = 0; - mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords); + mActivityContext.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords); outRect.offsetTo(sTempCords[0], sTempCords[1]); } @@ -273,7 +281,7 @@ public abstract class ButtonDropTarget extends TextView int viewHeight = dragObject.dragView.getMeasuredHeight(); int drawableWidth = mDrawable.getIntrinsicWidth(); int drawableHeight = mDrawable.getIntrinsicHeight(); - DragLayer dragLayer = mLauncher.getDragLayer(); + DragLayer dragLayer = mDropTargetHandler.getDragLayer(); // Find the rect to animate to (the view is center aligned) Rect to = new Rect(); @@ -314,7 +322,7 @@ public abstract class ButtonDropTarget extends TextView @Override public void onClick(View v) { - mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null); + mDropTargetHandler.onClick(this); } public void setTextVisible(boolean isVisible) { @@ -407,7 +415,8 @@ public abstract class ButtonDropTarget extends TextView /** * Returns if the text will be clipped vertically within the provided availableHeight. */ - private boolean isTextClippedVertically(int availableHeight) { + @VisibleForTesting + protected boolean isTextClippedVertically(int availableHeight) { availableHeight -= getPaddingTop() + getPaddingBottom(); if (availableHeight <= 0) { return true; diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java index 9ef9320352..9a5627a28c 100644 --- a/src/com/android/launcher3/DeleteDropTarget.java +++ b/src/com/android/launcher3/DeleteDropTarget.java @@ -18,24 +18,19 @@ package com.android.launcher3; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_CANCEL; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_REMOVE; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNDO; import android.content.Context; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.util.IntSet; -import com.android.launcher3.views.Snackbar; public class DeleteDropTarget extends ButtonDropTarget { @@ -43,6 +38,10 @@ public class DeleteDropTarget extends ButtonDropTarget { private StatsLogManager.LauncherEvent mLauncherEvent; + public DeleteDropTarget(Context context) { + this(context, null, 0); + } + public DeleteDropTarget(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -120,7 +119,7 @@ public class DeleteDropTarget extends ButtonDropTarget { @Override public void onDrop(DragObject d, DragOptions options) { if (canRemove(d.dragInfo)) { - mLauncher.getModelWriter().prepareToUndoDelete(); + mDropTargetHandler.prepareToUndoDelete(); } super.onDrop(d, options); mStatsLogManager.logger().withInstanceId(d.logInstanceId) @@ -131,26 +130,8 @@ public class DeleteDropTarget extends ButtonDropTarget { public void completeDrop(DragObject d) { ItemInfo item = d.dragInfo; if (canRemove(item)) { - ItemInfo pageItem = item; - if (item.container <= 0) { - View v = mLauncher.getWorkspace().getHomescreenIconByItemId(item.container); - if (v != null) { - pageItem = (ItemInfo) v.getTag(); - } - } - IntSet pageIds = pageItem.container == Favorites.CONTAINER_DESKTOP - ? IntSet.wrap(pageItem.screenId) - : mLauncher.getWorkspace().getCurrentPageScreenIds(); - onAccessibilityDrop(null, item); - ModelWriter modelWriter = mLauncher.getModelWriter(); - Runnable onUndoClicked = () -> { - mLauncher.setPagesToBindSynchronously(pageIds); - modelWriter.abortDelete(); - mLauncher.getStatsLogManager().logger().log(LAUNCHER_UNDO); - }; - Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, - modelWriter::commitDelete, onUndoClicked); + mDropTargetHandler.onDeleteComplete(item); } } @@ -162,9 +143,7 @@ public class DeleteDropTarget extends ButtonDropTarget { // Remove the item from launcher and the db, we can ignore the containerInfo in this call // because we already remove the drag view from the folder (if the drag originated from // a folder) in Folder.beginDrag() - mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop"); - mLauncher.getWorkspace().stripEmptyScreens(); - mLauncher.getDragLayer() - .announceForAccessibility(getContext().getString(R.string.item_removed)); + CharSequence announcement = getContext().getString(R.string.item_removed); + mDropTargetHandler.onAccessibilityDelete(view, item, announcement); } } diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt new file mode 100644 index 0000000000..277f8b37db --- /dev/null +++ b/src/com/android/launcher3/DropTargetHandler.kt @@ -0,0 +1,119 @@ +package com.android.launcher3 + +import android.content.ComponentName +import android.view.View +import com.android.launcher3.DropTarget.DragObject +import com.android.launcher3.SecondaryDropTarget.DeferredOnComplete +import com.android.launcher3.dragndrop.DragLayer +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.ModelWriter +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.util.IntSet +import com.android.launcher3.util.PendingRequestArgs +import com.android.launcher3.views.Snackbar + +/** + * Handler class for drop target actions that require modifying or interacting with launcher. + * + * This class is created by Launcher and provided the instance of launcher when created, which + * allows us to decouple drop target controllers from Launcher to enable easier testing. + */ +class DropTargetHandler(launcher: Launcher) { + val mLauncher: Launcher = launcher + + val modelWriter: ModelWriter = mLauncher.modelWriter + + fun onDropAnimationComplete() { + mLauncher.stateManager.goToState(LauncherState.NORMAL) + } + + fun onSecondaryTargetCompleteDrop(target: ComponentName?, d: DragObject) { + when (val dragSource = d.dragSource) { + is DeferredOnComplete -> { + val deferred: DeferredOnComplete = dragSource + if (d.dragSource is SecondaryDropTarget.DeferredOnComplete) { + target?.let { + deferred.mPackageName = it.packageName + mLauncher.addOnResumeCallback { deferred.onLauncherResume() } + } + ?: deferred.sendFailure() + } + } + } + } + + fun reconfigureWidget(widgetId: Int, info: ItemInfo) { + mLauncher.setWaitingForResult(PendingRequestArgs.forWidgetInfo(widgetId, null, info)) + mLauncher.appWidgetHolder.startConfigActivity( + mLauncher, + widgetId, + Launcher.REQUEST_RECONFIGURE_APPWIDGET + ) + } + + fun dismissPrediction( + announcement: CharSequence, + onActionClicked: Runnable, + onDismiss: Runnable? + ) { + mLauncher.dragLayer.announceForAccessibility(announcement) + Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, onDismiss, onActionClicked) + } + + fun getViewUnderDrag(info: ItemInfo): View? { + return if ( + info is LauncherAppWidgetInfo && + info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && + mLauncher.workspace.dragInfo != null + ) { + mLauncher.workspace.dragInfo.cell + } else null + } + + fun prepareToUndoDelete() { + mLauncher.modelWriter.prepareToUndoDelete() + } + + fun onDeleteComplete(item: ItemInfo) { + var pageItem: ItemInfo = item + if (item.container <= 0) { + val v = mLauncher.workspace.getHomescreenIconByItemId(item.container) + v?.let { pageItem = v.tag as ItemInfo } + } + val pageIds = + if (pageItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) + IntSet.wrap(pageItem.screenId) + else mLauncher.workspace.currentPageScreenIds + val onUndoClicked = Runnable { + mLauncher.setPagesToBindSynchronously(pageIds) + modelWriter.abortDelete() + mLauncher.statsLogManager.logger().log(LauncherEvent.LAUNCHER_UNDO) + } + + Snackbar.show( + mLauncher, + R.string.item_removed, + R.string.undo, + modelWriter::commitDelete, + onUndoClicked + ) + } + + fun onAccessibilityDelete(view: View?, item: ItemInfo, announcement: CharSequence) { + // Remove the item from launcher and the db, we can ignore the containerInfo in this call + // because we already remove the drag view from the folder (if the drag originated from + // a folder) in Folder.beginDrag() + mLauncher.removeItem(view, item, true /* deleteFromDb */, "removed by accessibility drop") + mLauncher.workspace.stripEmptyScreens() + mLauncher.dragLayer.announceForAccessibility(announcement) + } + + fun getDragLayer(): DragLayer { + return mLauncher.dragLayer + } + + fun onClick(buttonDropTarget: ButtonDropTarget) { + mLauncher.accessibilityDelegate.handleAccessibleDrop(buttonDropTarget, null, null) + } +} diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 7beac0b65c..96d713abbe 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1798,6 +1798,11 @@ public class Launcher extends StatefulActivity return mDragController; } + @Override + public DropTargetHandler getDropTargetHandler() { + return new DropTargetHandler(this); + } + @Override public void startActivityForResult(Intent intent, int requestCode, Bundle options) { if (requestCode != -1) { diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index 791cfff9b0..2dd610cbf9 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -3,8 +3,6 @@ package com.android.launcher3; import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID; import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE; -import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET; -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DISMISS_PREDICTION; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.INVALID; import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.RECONFIGURE; @@ -45,10 +43,7 @@ import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.util.PackageManagerHelper; -import com.android.launcher3.util.PendingRequestArgs; -import com.android.launcher3.views.Snackbar; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import java.net.URISyntaxException; @@ -204,7 +199,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList user = item.user; } if (intent != null) { - LauncherActivityInfo info = mLauncher.getSystemService(LauncherApps.class) + LauncherActivityInfo info = getContext().getSystemService(LauncherApps.class) .resolveActivity(intent, user); if (info != null && (info.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 0) { @@ -239,23 +234,11 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList public void completeDrop(final DragObject d) { ComponentName target = performDropAction(getViewUnderDrag(d.dragInfo), d.dragInfo, d.logInstanceId); - if (d.dragSource instanceof DeferredOnComplete) { - DeferredOnComplete deferred = (DeferredOnComplete) d.dragSource; - if (target != null) { - deferred.mPackageName = target.getPackageName(); - mLauncher.addOnResumeCallback(deferred::onLauncherResume); - } else { - deferred.sendFailure(); - } - } + mDropTargetHandler.onSecondaryTargetCompleteDrop(target, d); } private View getViewUnderDrag(ItemInfo info) { - if (info instanceof LauncherAppWidgetInfo && info.container == CONTAINER_DESKTOP && - mLauncher.getWorkspace().getDragInfo() != null) { - return mLauncher.getWorkspace().getDragInfo().cell; - } - return null; + return mDropTargetHandler.getViewUnderDrag(info); } /** @@ -286,18 +269,15 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList if (mCurrentAccessibilityAction == RECONFIGURE) { int widgetId = getReconfigurableWidgetId(view); if (widgetId != INVALID_APPWIDGET_ID) { - mLauncher.setWaitingForResult( - PendingRequestArgs.forWidgetInfo(widgetId, null, info)); - mLauncher.getAppWidgetHolder().startConfigActivity(mLauncher, widgetId, - REQUEST_RECONFIGURE_APPWIDGET); + mDropTargetHandler.reconfigureWidget(widgetId, info); } return null; } if (mCurrentAccessibilityAction == DISMISS_PREDICTION) { if (FeatureFlags.ENABLE_DISMISS_PREDICTION_UNDO.get()) { - mLauncher.getDragLayer() - .announceForAccessibility(getContext().getString(R.string.item_removed)); - Snackbar.show(mLauncher, R.string.item_removed, R.string.undo, () -> { }, () -> { + CharSequence announcement = getContext().getString(R.string.item_removed); + mDropTargetHandler + .dismissPrediction(announcement, () -> {}, () -> { mStatsLogManager.logger() .withInstanceId(instanceId) .withItemInfo(info) @@ -306,20 +286,23 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList } return null; } - // else: mCurrentAccessibilityAction == UNINSTALL ComponentName cn = getUninstallTarget(info); if (cn == null) { // System applications cannot be installed. For now, show a toast explaining that. // We may give them the option of disabling apps this way. - Toast.makeText(mLauncher, R.string.uninstall_system_app_text, Toast.LENGTH_SHORT).show(); + Toast.makeText( + getContext(), + R.string.uninstall_system_app_text, + Toast.LENGTH_SHORT + ).show(); return null; } try { - Intent i = Intent.parseUri(mLauncher.getString(R.string.delete_package_intent), 0) + Intent i = Intent.parseUri(getContext().getString(R.string.delete_package_intent), 0) .setData(Uri.fromParts("package", cn.getPackageName(), cn.getClassName())) .putExtra(Intent.EXTRA_USER, info.user); - mLauncher.startActivity(i); + getContext().startActivity(i); FileLog.d(TAG, "start uninstall activity " + cn.getPackageName()); return cn; } catch (URISyntaxException e) { @@ -339,12 +322,12 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList * A wrapper around {@link DragSource} which delays the {@link #onDropCompleted} action until * {@link #onLauncherResume} */ - private class DeferredOnComplete implements DragSource { + protected class DeferredOnComplete implements DragSource { private final DragSource mOriginal; private final Context mContext; - private String mPackageName; + protected String mPackageName; private DragObject mDragObject; public DeferredOnComplete(DragSource original, Context context) { diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java index a6744fb02a..2d09ec1409 100644 --- a/src/com/android/launcher3/views/ActivityContext.java +++ b/src/com/android/launcher3/views/ActivityContext.java @@ -53,6 +53,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.DropTargetHandler; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -188,6 +189,13 @@ public interface ActivityContext { return null; } + /** + * Handler for actions taken on drop targets that require launcher + */ + default DropTargetHandler getDropTargetHandler() { + return null; + } + /** * Returns the FolderIcon with the given item id, if it exists. */ diff --git a/tests/src/com/android/launcher3/DeleteDropTargetTest.kt b/tests/src/com/android/launcher3/DeleteDropTargetTest.kt new file mode 100644 index 0000000000..a588554d6a --- /dev/null +++ b/tests/src/com/android/launcher3/DeleteDropTargetTest.kt @@ -0,0 +1,42 @@ +package com.android.launcher3 + +import android.content.Context +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.Utilities.* +import com.android.launcher3.util.ActivityContextWrapper +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeleteDropTargetTest { + + private var mContext: Context = ActivityContextWrapper(getApplicationContext()) + + // Use a non-abstract class implementation + private var buttonDropTarget: DeleteDropTarget = DeleteDropTarget(mContext) + + @Before + fun setup() { + enableRunningInTestHarnessForTests() + } + + // Needs mText, mTempRect, getPaddingTop, getPaddingBottom + // availableHeight as a parameter + @Test + fun isTextClippedVerticallyTest() { + buttonDropTarget.mText = "My Test" + // No space for text + assertThat(buttonDropTarget.isTextClippedVertically(30)).isTrue() + + // Some space for text, and just enough that the text should not be clipped + assertThat(buttonDropTarget.isTextClippedVertically(50)).isFalse() + + // A lot of space for text so the text should not be clipped + assertThat(buttonDropTarget.isTextClippedVertically(100)).isFalse() + } +}