Merge "Unit testing ButtonDropTarget" into udc-dev

This commit is contained in:
Andrew Cole
2023-04-20 23:35:49 +00:00
committed by Android (Google) Code Review
7 changed files with 220 additions and 75 deletions
+22 -13
View File
@@ -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;
@@ -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);
}
}
@@ -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)
}
}
+5
View File
@@ -1770,6 +1770,11 @@ public class Launcher extends StatefulActivity<LauncherState>
return mDragController;
}
@Override
public DropTargetHandler getDropTargetHandler() {
return new DropTargetHandler(this);
}
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
if (requestCode != -1) {
@@ -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) {
@@ -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.
*/
@@ -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()
}
}