Add undo snackbar for deleting items

- Add methods to ModelWriter to prepareForUndoDelete, then
  enqueueDeleteRunnable, followed by commitDelete or abortDelete.
- Add Snackbar floating view
- Show Undo snackbar when dropping or flinging to delete target; if the
  undo action is clicked, we abort the delete, otherwise we commit it.

Bug: 24238108
Change-Id: I9997235e1f8525cbb8b1fa2338099609e7358426
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 1c07ea4..4fe6099 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -19,7 +19,6 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
-
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 
@@ -54,6 +53,7 @@
             TYPE_WIDGETS_FULL_SHEET,
             TYPE_ON_BOARD_POPUP,
             TYPE_DISCOVERY_BOUNCE,
+            TYPE_SNACKBAR,
 
             TYPE_QUICKSTEP_PREVIEW,
             TYPE_TASK_MENU,
@@ -68,23 +68,25 @@
     public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
     public static final int TYPE_ON_BOARD_POPUP = 1 << 5;
     public static final int TYPE_DISCOVERY_BOUNCE = 1 << 6;
+    public static final int TYPE_SNACKBAR = 1 << 7;
 
     // Popups related to quickstep UI
-    public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 7;
-    public static final int TYPE_TASK_MENU = 1 << 8;
-    public static final int TYPE_OPTIONS_POPUP = 1 << 9;
+    public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 8;
+    public static final int TYPE_TASK_MENU = 1 << 9;
+    public static final int TYPE_OPTIONS_POPUP = 1 << 10;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
-            | TYPE_OPTIONS_POPUP;
+            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
             | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE;
 
     // Usually we show the back button when a floating view is open. Instead, hide for these types.
-    public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE;
+    public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
+            | TYPE_SNACKBAR;
 
     public static final int TYPE_ACCESSIBLE = TYPE_ALL
             & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_QUICKSTEP_PREVIEW;
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 2ec7e01..7b56527 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -17,7 +17,6 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.app.Activity;
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index ed8c42d..dd63ebc 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -264,6 +264,10 @@
      */
     @Override
     public void onDrop(final DragObject d, final DragOptions options) {
+        if (options.isFlingToDelete) {
+            // FlingAnimation handles the animation and then calls completeDrop().
+            return;
+        }
         final DragLayer dragLayer = mLauncher.getDragLayer();
         final Rect from = new Rect();
         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 64a58fb..c80f96b 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -23,10 +23,11 @@
 
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.folder.Folder;
 import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.views.Snackbar;
 
 public class DeleteDropTarget extends ButtonDropTarget {
 
@@ -81,13 +82,17 @@
      */
     private void setTextBasedOnDragSource(ItemInfo item) {
         if (!TextUtils.isEmpty(mText)) {
-            mText = getResources().getString(item.id != ItemInfo.NO_ID
+            mText = getResources().getString(canRemove(item)
                     ? R.string.remove_drop_target_label
                     : android.R.string.cancel);
             requestLayout();
         }
     }
 
+    private boolean canRemove(ItemInfo item) {
+        return item.id != ItemInfo.NO_ID;
+    }
+
     /**
      * Set mControlType depending on the drag item.
      */
@@ -97,10 +102,21 @@
     }
 
     @Override
+    public void onDrop(DragObject d, DragOptions options) {
+        if (canRemove(d.dragInfo)) {
+            mLauncher.getModelWriter().prepareToUndoDelete();
+        }
+        super.onDrop(d, options);
+    }
+
+    @Override
     public void completeDrop(DragObject d) {
         ItemInfo item = d.dragInfo;
-        if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) {
+        if (canRemove(item)) {
             onAccessibilityDrop(null, item);
+            ModelWriter modelWriter = mLauncher.getModelWriter();
+            Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
+                    modelWriter::commitDelete, modelWriter::abortDelete);
         }
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index f0ddd53..55074f8 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -19,7 +19,7 @@
 import static android.content.pm.ActivityInfo.CONFIG_LOCALE;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
-
+import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -48,7 +48,6 @@
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -1549,7 +1548,7 @@
             final LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) itemInfo;
             mWorkspace.removeWorkspaceItem(v);
             if (deleteFromDb) {
-                deleteWidgetInfo(widgetInfo);
+                getModelWriter().deleteWidgetInfo(widgetInfo, getAppWidgetHost());
             }
         } else {
             return false;
@@ -1557,23 +1556,7 @@
         return true;
     }
 
-    /**
-     * Deletes the widget info and the widget id.
-     */
-    private void deleteWidgetInfo(final LauncherAppWidgetInfo widgetInfo) {
-        final LauncherAppWidgetHost appWidgetHost = getAppWidgetHost();
-        if (appWidgetHost != null && !widgetInfo.isCustomWidget() && widgetInfo.isWidgetIdAllocated()) {
-            // Deleting an app widget ID is a void call but writes to disk before returning
-            // to the caller...
-            new AsyncTask<Void, Void, Void>() {
-                public Void doInBackground(Void ... args) {
-                    appWidgetHost.deleteAppWidgetId(widgetInfo.appWidgetId);
-                    return null;
-                }
-            }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
-        }
-        getModelWriter().deleteItemFromDatabase(widgetInfo);
-    }
+
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
@@ -1808,6 +1791,17 @@
     }
 
     @Override
+    public void preAddApps() {
+        // If there's an undo snackbar, force it to complete to ensure empty screens are removed
+        // before trying to add new items.
+        mModelWriter.commitDelete();
+        AbstractFloatingView snackbar = AbstractFloatingView.getOpenView(this, TYPE_SNACKBAR);
+        if (snackbar != null) {
+            snackbar.post(() -> snackbar.close(true));
+        }
+    }
+
+    @Override
     public void bindAppsAdded(ArrayList<Long> newScreens, ArrayList<ItemInfo> addNotAnimated,
             ArrayList<ItemInfo> addAnimated) {
         // Add the new screens
@@ -2040,7 +2034,7 @@
             // Verify that we own the widget
             if (appWidgetInfo == null) {
                 FileLog.e(TAG, "Removing invalid widget: id=" + item.appWidgetId);
-                deleteWidgetInfo(item);
+                getModelWriter().deleteWidgetInfo(item, getAppWidgetHost());
                 return null;
             }
 
@@ -2130,7 +2124,7 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    public void finishBindingItems() {
+    public void finishBindingItems(int currentScreen) {
         TraceHelper.beginSection("finishBindingItems");
         mWorkspace.restoreInstanceStateForRemainingPages();
 
@@ -2145,6 +2139,8 @@
         InstallShortcutReceiver.disableAndFlushInstallQueue(
                 InstallShortcutReceiver.FLAG_LOADER_RUNNING, this);
 
+        mWorkspace.setCurrentPage(currentScreen);
+
         TraceHelper.endSection("finishBindingItems");
     }
 
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index fe9f901..7a90a55 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -144,9 +144,10 @@
         public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
         public void bindScreens(ArrayList<Long> orderedScreenIds);
         public void finishFirstPageBind(ViewOnDrawExecutor executor);
-        public void finishBindingItems();
+        public void finishBindingItems(int currentScreen);
         public void bindAllApplications(ArrayList<AppInfo> apps);
         public void bindAppsAddedOrUpdated(ArrayList<AppInfo> apps);
+        public void preAddApps();
         public void bindAppsAdded(ArrayList<Long> newScreens,
                                   ArrayList<ItemInfo> addNotAnimated,
                                   ArrayList<ItemInfo> addAnimated);
@@ -196,6 +197,10 @@
      * Adds the provided items to the workspace.
      */
     public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
+        Callbacks callbacks = getCallback();
+        if (callbacks != null) {
+            callbacks.preAddApps();
+        }
         enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
     }
 
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 76e85e2..6083415 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -2,7 +2,6 @@
 
 import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
 import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE;
-
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_MASK;
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_SYSTEM_NO;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
@@ -10,7 +9,6 @@
 import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.UNINSTALL;
 
 import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index d652fe0..353916f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -819,7 +819,9 @@
 
         if (!removeScreens.isEmpty()) {
             // Update the model if we have changed any screens
-            LauncherModel.updateWorkspaceScreenOrder(mLauncher, mScreenOrder);
+            mLauncher.getModelWriter().enqueueDeleteRunnable(
+                    () -> LauncherModel.updateWorkspaceScreenOrder(mLauncher, mScreenOrder));
+
         }
 
         if (pageShift >= 0) {
@@ -2848,7 +2850,6 @@
      */
     public void onDropCompleted(final View target, final DragObject d,
             final boolean success) {
-
         if (success) {
             if (target != this && mDragInfo != null) {
                 removeWorkspaceItem(mDragInfo.cell);
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 8007754..03dc66e 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -396,7 +396,7 @@
     @Override
     public void onDriverDragEnd(float x, float y) {
         DropTarget dropTarget;
-        Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject);
+        Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject, mOptions);
         if (flingAnimation != null) {
             dropTarget = mFlingToDeleteHelper.getDropTarget();
         } else {
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index f108f8b..2d19f36 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -37,6 +37,8 @@
     /** Scale of the icons over the workspace icon size. */
     public float intrinsicIconScaleFactor = 1f;
 
+    public boolean isFlingToDelete;
+
     /**
      * Specifies a condition that must be met before DragListener#onDragStart() is called.
      * By default, there is no condition and onDragStart() is called immediately following
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index e794744..589ad25 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -91,12 +91,13 @@
         return mDropTarget;
     }
 
-    public Runnable getFlingAnimation(DropTarget.DragObject dragObject) {
+    public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) {
         PointF vel = isFlingingToDelete();
-        if (vel == null) {
+        options.isFlingToDelete = vel != null;
+        if (!options.isFlingToDelete) {
             return null;
         }
-        return new FlingAnimation(dragObject, vel, mDropTarget, mLauncher);
+        return new FlingAnimation(dragObject, vel, mDropTarget, mLauncher, options);
     }
 
     /**
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index a059627..c4d1058 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -25,7 +25,6 @@
 import android.animation.AnimatorSet;
 import android.annotation.SuppressLint;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.text.InputType;
@@ -41,7 +40,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
-import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AnimationUtils;
 import android.view.inputmethod.EditorInfo;
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 6ef798d..1277a20 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -155,6 +155,7 @@
         final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary);
         final int initialColor =
                 ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha());
+        mFolderBackground.mutate();
         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
 
         // Set up the reveal animation that clips the Folder.
diff --git a/src/com/android/launcher3/model/LoaderResults.java b/src/com/android/launcher3/model/LoaderResults.java
index 0fd9b73..5778936 100644
--- a/src/com/android/launcher3/model/LoaderResults.java
+++ b/src/com/android/launcher3/model/LoaderResults.java
@@ -181,7 +181,7 @@
             public void run() {
                 Callbacks callbacks = mCallbacks.get();
                 if (callbacks != null) {
-                    callbacks.finishBindingItems();
+                    callbacks.finishBindingItems(currentScreen);
                 }
             }
         };
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index eba7515..9f6e4f8 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -28,6 +28,8 @@
 import com.android.launcher3.FolderInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetHost;
+import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.Callbacks;
 import com.android.launcher3.LauncherProvider;
@@ -35,13 +37,14 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.ShortcutInfo;
-import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LooperExecutor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -60,6 +63,10 @@
     private final boolean mHasVerticalHotseat;
     private final boolean mVerifyChanges;
 
+    // Keep track of delete operations that occur when an Undo option is present; we may not commit.
+    private final List<Runnable> mDeleteRunnables = new ArrayList<>();
+    private boolean mPreparingToUndo;
+
     public ModelWriter(Context context, LauncherModel model, BgDataModel dataModel,
             boolean hasVerticalHotseat, boolean verifyChanges) {
         mContext = context;
@@ -152,7 +159,7 @@
                 .put(Favorites.RANK, item.rank)
                 .put(Favorites.SCREEN, item.screenId);
 
-        mWorkerExecutor.execute(new UpdateItemRunnable(item, writer));
+        enqueueDeleteRunnable(new UpdateItemRunnable(item, writer));
     }
 
     /**
@@ -176,7 +183,7 @@
 
             contentValues.add(values);
         }
-        mWorkerExecutor.execute(new UpdateItemsRunnable(items, contentValues));
+        enqueueDeleteRunnable(new UpdateItemsRunnable(items, contentValues));
     }
 
     /**
@@ -258,7 +265,7 @@
     public void deleteItemsFromDatabase(final Iterable<? extends ItemInfo> items) {
         ModelVerifier verifier = new ModelVerifier();
 
-        mWorkerExecutor.execute(() -> {
+        enqueueDeleteRunnable(() -> {
             for (ItemInfo item : items) {
                 final Uri uri = Favorites.getContentUri(item.id);
                 mContext.getContentResolver().delete(uri, null, null);
@@ -275,7 +282,7 @@
     public void deleteFolderAndContentsFromDatabase(final FolderInfo info) {
         ModelVerifier verifier = new ModelVerifier();
 
-        mWorkerExecutor.execute(() -> {
+        enqueueDeleteRunnable(() -> {
             ContentResolver cr = mContext.getContentResolver();
             cr.delete(LauncherSettings.Favorites.CONTENT_URI,
                     LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
@@ -288,6 +295,63 @@
         });
     }
 
+    /**
+     * Deletes the widget info and the widget id.
+     */
+    public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherAppWidgetHost host) {
+        if (host != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) {
+            // Deleting an app widget ID is a void call but writes to disk before returning
+            // to the caller...
+            enqueueDeleteRunnable(() -> host.deleteAppWidgetId(info.appWidgetId));
+        }
+        deleteItemFromDatabase(info);
+    }
+
+    /**
+     * Delete operations tracked using {@link #enqueueDeleteRunnable} will only be called
+     * if {@link #commitDelete} is called. Note that one of {@link #commitDelete()} or
+     * {@link #abortDelete()} MUST be called after this method, or else all delete
+     * operations will remain uncommitted indefinitely.
+     */
+    public void prepareToUndoDelete() {
+        if (!mPreparingToUndo) {
+            if (!mDeleteRunnables.isEmpty() && FeatureFlags.IS_DOGFOOD_BUILD) {
+                throw new IllegalStateException("There are still uncommitted delete operations!");
+            }
+            mDeleteRunnables.clear();
+            mPreparingToUndo = true;
+        }
+    }
+
+    /**
+     * If {@link #prepareToUndoDelete} has been called, we store the Runnable to be run when
+     * {@link #commitDelete()} is called (or abandoned if {@link #abortDelete()} is called).
+     * Otherwise, we run the Runnable immediately.
+     */
+    public void enqueueDeleteRunnable(Runnable r) {
+        if (mPreparingToUndo) {
+            mDeleteRunnables.add(r);
+        } else {
+            mWorkerExecutor.execute(r);
+        }
+    }
+
+    public void commitDelete() {
+        mPreparingToUndo = false;
+        for (Runnable runnable : mDeleteRunnables) {
+            mWorkerExecutor.execute(runnable);
+        }
+        mDeleteRunnables.clear();
+    }
+
+    public void abortDelete() {
+        mPreparingToUndo = false;
+        mDeleteRunnables.clear();
+        // We do a full reload here instead of just a rebind because Folders change their internal
+        // state when dragging an item out, which clobbers the rebind unless we load from the DB.
+        mModel.forceReload();
+    }
+
     private class UpdateItemRunnable extends UpdateItemBaseRunnable {
         private final ItemInfo mItem;
         private final ContentWriter mWriter;
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 6f012f6..babbcdd 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -17,7 +17,6 @@
 
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
-
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -30,7 +29,6 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java
index fe0571b..9d0ad22 100644
--- a/src/com/android/launcher3/util/FlingAnimation.java
+++ b/src/com/android/launcher3/util/FlingAnimation.java
@@ -14,6 +14,7 @@
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 
 public class FlingAnimation implements AnimatorUpdateListener, Runnable {
@@ -28,6 +29,7 @@
     private final Launcher mLauncher;
 
     protected final DragObject mDragObject;
+    protected final DragOptions mDragOptions;
     protected final DragLayer mDragLayer;
     protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
     protected final float mUX, mUY;
@@ -39,13 +41,15 @@
 
     protected float mAX, mAY;
 
-    public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher) {
+    public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher,
+            DragOptions options) {
         mDropTarget = dropTarget;
         mLauncher = launcher;
         mDragObject = d;
         mUX = vel.x / 1000;
         mUY = vel.y / 1000;
         mDragLayer = mLauncher.getDragLayer();
+        mDragOptions = options;
     }
 
     @Override
@@ -102,6 +106,7 @@
             }
         };
 
+        mDropTarget.onDrop(mDragObject, mDragOptions);
         mDragLayer.animateView(mDragObject.dragView, this, duration, tInterpolator,
                 onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
     }
diff --git a/src/com/android/launcher3/views/Snackbar.java b/src/com/android/launcher3/views/Snackbar.java
new file mode 100644
index 0000000..f515360
--- /dev/null
+++ b/src/com/android/launcher3/views/Snackbar.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2018 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.views;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+
+/**
+ * A toast-like UI at the bottom of the screen with a label, button action, and dismiss action.
+ */
+public class Snackbar extends AbstractFloatingView {
+
+    private static final long SHOW_DURATION_MS = 180;
+    private static final long HIDE_DURATION_MS = 180;
+    private static final long TIMEOUT_DURATION_MS = 4000;
+
+    private final Launcher mLauncher;
+    private Runnable mOnDismissed;
+
+    public Snackbar(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public Snackbar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        inflate(context, R.layout.snackbar, this);
+    }
+
+    public static void show(Launcher launcher, int labelStringResId, int actionStringResId,
+            Runnable onDismissed, Runnable onActionClicked) {
+        closeOpenViews(launcher, true, TYPE_SNACKBAR);
+        Snackbar snackbar = new Snackbar(launcher, null);
+        // Set some properties here since inflated xml only contains the children.
+        snackbar.setOrientation(HORIZONTAL);
+        snackbar.setGravity(Gravity.CENTER_VERTICAL);
+        Resources res = launcher.getResources();
+        snackbar.setElevation(res.getDimension(R.dimen.deep_shortcuts_elevation));
+        int padding = res.getDimensionPixelSize(R.dimen.snackbar_padding);
+        snackbar.setPadding(padding, padding, padding, padding);
+        snackbar.setBackgroundResource(R.drawable.round_rect_primary);
+
+        snackbar.mIsOpen = true;
+        DragLayer dragLayer = launcher.getDragLayer();
+        dragLayer.addView(snackbar);
+
+        DragLayer.LayoutParams params = (DragLayer.LayoutParams) snackbar.getLayoutParams();
+        params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+        params.height = res.getDimensionPixelSize(R.dimen.snackbar_height);
+        int margin = res.getDimensionPixelSize(R.dimen.snackbar_margin);
+        Rect insets = launcher.getDeviceProfile().getInsets();
+        params.width = dragLayer.getWidth() - margin * 2 -  insets.left - insets.right;
+        params.setMargins(0, margin + insets.top, 0, margin + insets.bottom);
+
+        ((TextView) snackbar.findViewById(R.id.label)).setText(labelStringResId);
+        ((TextView) snackbar.findViewById(R.id.action)).setText(actionStringResId);
+        snackbar.findViewById(R.id.action).setOnClickListener(v -> {
+            if (onActionClicked != null) {
+                onActionClicked.run();
+            }
+            snackbar.mOnDismissed = null;
+            snackbar.close(true);
+        });
+        snackbar.mOnDismissed = onDismissed;
+
+        snackbar.setAlpha(0);
+        snackbar.setScaleX(0.8f);
+        snackbar.setScaleY(0.8f);
+        snackbar.animate()
+                .alpha(1f)
+                .withLayer()
+                .scaleX(1)
+                .scaleY(1)
+                .setDuration(SHOW_DURATION_MS)
+                .setInterpolator(Interpolators.ACCEL_DEACCEL)
+                .start();
+        snackbar.postDelayed(() -> snackbar.close(true), TIMEOUT_DURATION_MS);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (mIsOpen) {
+            if (animate) {
+                animate().alpha(0f)
+                        .withLayer()
+                        .setStartDelay(0)
+                        .setDuration(HIDE_DURATION_MS)
+                        .setInterpolator(Interpolators.ACCEL)
+                        .withEndAction(this::onClosed)
+                        .start();
+            } else {
+                animate().cancel();
+                onClosed();
+            }
+            mIsOpen = false;
+        }
+    }
+
+    private void onClosed() {
+        mLauncher.getDragLayer().removeView(this);
+        if (mOnDismissed != null) {
+            mOnDismissed.run();
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // TODO
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_SNACKBAR) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            DragLayer dl = mLauncher.getDragLayer();
+            if (!dl.isEventOverView(this, ev)) {
+                close(true);
+            }
+        }
+        return false;
+    }
+}