diff options
author | 2018-08-03 16:20:55 +0800 | |
---|---|---|
committer | 2018-08-14 16:28:25 +0800 | |
commit | ea3de8bea73d48fce5c7531bdd7e98dba608fc18 (patch) | |
tree | c6d98da03d0d4bd9d4af90fc833a9cbac3bcb83c | |
parent | a65ac0189b79e9617983674d053be1739b1749ab (diff) |
Support delete/undo in the File browser mode
Bug: 111863032
Test: atest FileUndoDeletionUiTest
Change-Id: If9a4a9ff8e44caa23d8f6ed140d6404080d76772
16 files changed, 308 insertions, 154 deletions
diff --git a/src/com/android/documentsui/ActionModeAddons.java b/src/com/android/documentsui/ActionModeAddons.java index 83eba78bb..ef8ae1999 100644 --- a/src/com/android/documentsui/ActionModeAddons.java +++ b/src/com/android/documentsui/ActionModeAddons.java @@ -21,6 +21,4 @@ package com.android.documentsui; public interface ActionModeAddons { void finishActionMode(); - - void finishOnConfirmed(int code); } diff --git a/src/com/android/documentsui/ActionModeController.java b/src/com/android/documentsui/ActionModeController.java index 1c371fe30..4e0baef45 100644 --- a/src/com/android/documentsui/ActionModeController.java +++ b/src/com/android/documentsui/ActionModeController.java @@ -29,8 +29,6 @@ import android.view.MenuItem; import android.view.View; import com.android.documentsui.MenuManager.SelectionDetails; -import com.android.documentsui.base.ConfirmationCallback; -import com.android.documentsui.base.ConfirmationCallback.Result; import com.android.documentsui.base.EventHandler; import com.android.documentsui.base.Menus; import com.android.documentsui.selection.Selection; @@ -185,13 +183,6 @@ public class ActionModeController extends SelectionObserver } } - @Override - public void finishOnConfirmed(@Result int code) { - if (code == ConfirmationCallback.CONFIRM) { - finishActionMode(); - } - } - public ActionModeController reset( SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker) { assert(mActionMode == null); diff --git a/src/com/android/documentsui/Metrics.java b/src/com/android/documentsui/Metrics.java index 838ebf8b7..12df0bf1d 100644 --- a/src/com/android/documentsui/Metrics.java +++ b/src/com/android/documentsui/Metrics.java @@ -318,6 +318,7 @@ public final class Metrics { public static final int USER_ACTION_EXTRACT_TO = 28; public static final int USER_ACTION_VIEW_IN_APPLICATION = 29; public static final int USER_ACTION_INSPECTOR = 30; + public static final int USER_ACTION_UNDO_DELETE = 31; @IntDef(flag = false, value = { USER_ACTION_OTHER, @@ -349,7 +350,8 @@ public final class Metrics { USER_ACTION_COMPRESS, USER_ACTION_EXTRACT_TO, USER_ACTION_VIEW_IN_APPLICATION, - USER_ACTION_INSPECTOR + USER_ACTION_INSPECTOR, + USER_ACTION_UNDO_DELETE }) @Retention(RetentionPolicy.SOURCE) public @interface UserAction {} diff --git a/src/com/android/documentsui/Model.java b/src/com/android/documentsui/Model.java index 96dfc5dec..f02c324a1 100644 --- a/src/com/android/documentsui/Model.java +++ b/src/com/android/documentsui/Model.java @@ -32,7 +32,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import android.util.Log; -import com.android.documentsui.DirectoryResult; import com.android.documentsui.base.DocumentFilters; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.EventListener; @@ -45,6 +44,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -73,6 +73,7 @@ public class Model { private @Nullable Cursor mCursor; private int mCursorCount; private String mIds[] = new String[0]; + private Set<Selection> mDocumentsToBeDeleted = new HashSet<>(); public Model(Features features) { mFeatures = features; @@ -109,13 +110,13 @@ public class Model { doc = null; mIsLoading = false; mFileNames.clear(); + mDocumentsToBeDeleted.clear(); notifyUpdateListeners(); } @VisibleForTesting protected void update(DirectoryResult result) { assert(result != null); - if (DEBUG) Log.i(TAG, "Updating model with new result set."); if (result.exception != null) { @@ -141,9 +142,56 @@ public class Model { notifyUpdateListeners(); } + public void markDocumentsToBeDeleted(Selection selection) { + if (mDocumentsToBeDeleted.contains(selection)) { + return; + } + mDocumentsToBeDeleted.add(selection); + updateModelData(); + notifyUpdateListeners(); + } + + public void restoreDocumentsToBeDeleted(Selection selection) { + if (!mDocumentsToBeDeleted.contains(selection)) { + return; + } + mDocumentsToBeDeleted.remove(selection); + updateModelData(); + notifyUpdateListeners(); + } + + private boolean isDocumentToBeDeleted(String id) { + for (Selection s : mDocumentsToBeDeleted) { + if (s.contains(id)) { + return true; + } + } + return false; + } + + private void updateDocumentsToBeDeleted() { + for (Iterator<Selection> i = mDocumentsToBeDeleted.iterator(); i.hasNext();) { + Selection selection = i.next(); + for (String id : selection) { + if (!mPositions.containsKey(id)) { + i.remove(); + break; + } + } + } + } + + private int getDocumentsToBeDeletedCount() { + int count = 0; + for (Selection s : mDocumentsToBeDeleted) { + count += s.size(); + } + return count; + } + @VisibleForTesting public int getItemCount() { - return mCursorCount; + return mCursorCount - getDocumentsToBeDeletedCount(); } /** @@ -151,9 +199,10 @@ public class Model { * according to the current sort order. */ private void updateModelData() { - mIds = new String[mCursorCount]; mFileNames.clear(); mCursor.moveToPosition(-1); + mPositions.clear(); + String[] tmpIds = new String[mCursorCount]; for (int pos = 0; pos < mCursorCount; ++pos) { if (!mCursor.moveToNext()) { Log.e(TAG, "Fail to move cursor to next pos: " + pos); @@ -164,18 +213,24 @@ public class Model { // If the cursor is a merged cursor over multiple authorities, then prefix the ids // with the authority to avoid collisions. if (mCursor instanceof MergeCursor) { - mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) + tmpIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); } else { - mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); + tmpIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); } + mPositions.put(tmpIds[pos], pos); mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME)); } - // Populate the positions. - mPositions.clear(); + updateDocumentsToBeDeleted(); + + mIds = new String[mCursorCount - getDocumentsToBeDeletedCount()]; + int index = 0; for (int i = 0; i < mCursorCount; ++i) { - mPositions.put(mIds[i], i); + if (!isDocumentToBeDeleted(tmpIds[i])) { + mIds[index] = tmpIds[i]; + index++; + } } } diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java index 21926561f..19c812ffd 100644 --- a/src/com/android/documentsui/files/ActionHandler.java +++ b/src/com/android/documentsui/files/ActionHandler.java @@ -25,11 +25,11 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import android.view.DragEvent; +import android.view.View; import com.android.documentsui.AbstractActionHandler; import com.android.documentsui.ActionModeAddons; @@ -42,8 +42,6 @@ import com.android.documentsui.Metrics; import com.android.documentsui.Model; import com.android.documentsui.R; import com.android.documentsui.TimeoutTask; -import com.android.documentsui.base.ConfirmationCallback; -import com.android.documentsui.base.ConfirmationCallback.Result; import com.android.documentsui.base.DebugFlags; import com.android.documentsui.base.DocumentFilters; import com.android.documentsui.base.DocumentInfo; @@ -69,11 +67,15 @@ import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperations; import com.android.documentsui.ui.DialogController; +import com.android.documentsui.ui.Snackbars; + import androidx.annotation.VisibleForTesting; +import android.support.design.widget.Snackbar; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -93,6 +95,8 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa private final DragAndDropManager mDragAndDropManager; private final Model mModel; + private Snackbar mDeletionSnackbar; + ActionHandler( T activity, State state, @@ -301,44 +305,57 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa final @Nullable DocumentInfo srcParent = mState.stack.peek(); - // Model must be accessed in UI thread, since underlying cursor is not threadsafe. - List<DocumentInfo> docs = mModel.getDocuments(selection); - - ConfirmationCallback result = (@Result int code) -> { - // share the news with our caller, be it good or bad. - mActionModeAddons.finishOnConfirmed(code); - - if (code != ConfirmationCallback.CONFIRM) { - return; - } - - UrisSupplier srcs; - try { - srcs = UrisSupplier.create( - selection, - mModel::getItemUri, - mClipStore); - } catch (Exception e) { - Log.e(TAG,"Failed to delete a file because we were unable to get item URIs.", e); - mDialogs.showFileOperationStatus( - FileOperations.Callback.STATUS_FAILED, - FileOperationService.OPERATION_DELETE, - selection.size()); - return; + UrisSupplier srcs; + try { + srcs = UrisSupplier.create( + selection, + mModel::getItemUri, + mClipStore); + } catch (Exception e) { + Log.e(TAG, "Failed to delete a file because we were unable to get item URIs.", e); + mDialogs.showFileOperationStatus( + FileOperations.Callback.STATUS_FAILED, + FileOperationService.OPERATION_DELETE, + selection.size()); + return; + } + mModel.markDocumentsToBeDeleted(selection); + Consumer<View> action = v -> { + Metrics.logUserAction(mActivity, Metrics.USER_ACTION_UNDO_DELETE); + mModel.restoreDocumentsToBeDeleted(selection); + }; + Snackbar.Callback callback = new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + super.onDismissed(snackbar, event); + if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) { + FileOperation operation = new FileOperation.Builder() + .withOpType(FileOperationService.OPERATION_DELETE) + .withDestination(mState.stack) + .withSrcs(srcs) + .withSrcParent(srcParent == null ? null : srcParent.derivedUri) + .build(); + + FileOperations.start(mActivity, operation, null, + FileOperations.createJobId()); + } + if (mDeletionSnackbar == snackbar) { + mDeletionSnackbar = null; + } } - - FileOperation operation = new FileOperation.Builder() - .withOpType(FileOperationService.OPERATION_DELETE) - .withDestination(mState.stack) - .withSrcs(srcs) - .withSrcParent(srcParent == null ? null : srcParent.derivedUri) - .build(); - - FileOperations.start(mActivity, operation, mDialogs::showFileOperationStatus, - FileOperations.createJobId()); }; + mDeletionSnackbar = showDeletionSnackbar(mActivity, selection.size(), action, callback); + } - mDialogs.confirmDelete(docs, result); + public Snackbar showDeletionSnackbar(Activity activity, int docCount, Consumer<View> action, + Snackbar.Callback callback) { + return Snackbars.showDelete(mActivity, docCount, action, callback); + } + + public void dismissDeletionSnackBar() { + if (mDeletionSnackbar != null) { + mDeletionSnackbar.dismiss(); + } } @Override @@ -394,6 +411,12 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa } @Override + public void loadDocumentsForCurrentStack() { + dismissDeletionSnackBar(); + super.loadDocumentsForCurrentStack(); + } + + @Override public void initLocation(Intent intent) { assert(intent != null); diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java index 3f1f47495..f27f0174f 100644 --- a/src/com/android/documentsui/files/FilesActivity.java +++ b/src/com/android/documentsui/files/FilesActivity.java @@ -262,6 +262,12 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons } @Override + protected void onPause() { + super.onPause(); + mInjector.actions.dismissDeletionSnackBar(); + } + + @Override public String getDrawerTitle() { Intent intent = getIntent(); return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE)) diff --git a/src/com/android/documentsui/services/FileOperations.java b/src/com/android/documentsui/services/FileOperations.java index edb6d3757..5c9c62b37 100644 --- a/src/com/android/documentsui/services/FileOperations.java +++ b/src/com/android/documentsui/services/FileOperations.java @@ -62,9 +62,10 @@ public final class FileOperations { String newJobId = jobId != null ? jobId : createJobId(); Intent intent = createBaseIntent(context, newJobId, operation); - - callback.onOperationResult(Callback.STATUS_ACCEPTED, operation.getOpType(), - operation.getSrc().getItemCount()); + if (callback != null) { + callback.onOperationResult(Callback.STATUS_ACCEPTED, operation.getOpType(), + operation.getSrc().getItemCount()); + } context.startService(intent); diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java index 0de98683f..24a0b5155 100644 --- a/src/com/android/documentsui/ui/DialogController.java +++ b/src/com/android/documentsui/ui/DialogController.java @@ -24,7 +24,6 @@ import android.widget.Button; import android.widget.TextView; import com.android.documentsui.R; -import com.android.documentsui.base.ConfirmationCallback; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Features; import com.android.documentsui.picker.OverwriteConfirmFragment; @@ -35,12 +34,9 @@ import com.android.documentsui.services.FileOperations.Callback.Status; import com.android.documentsui.services.FileOperations; import java.util.List; -import javax.annotation.Nullable; public interface DialogController { - // Dialogs used in FilesActivity - void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback); void showFileOperationStatus(int status, int opType, int docCount); /** @@ -72,44 +68,7 @@ public interface DialogController { } @Override - public void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback) { - assert(!docs.isEmpty()); - - TextView message = - (TextView) mActivity.getLayoutInflater().inflate( - R.layout.dialog_delete_confirmation, null); - message.setText(mMessages.generateDeleteMessage(docs)); - - // For now, we implement this dialog NOT - // as a fragment (which can survive rotation and have its own state), - // but as a simple runtime dialog. So rotating a device with an - // active delete dialog...results in that dialog disappearing. - // We can do better, but don't have cycles for it now. - final AlertDialog alertDialog = new AlertDialog.Builder(mActivity) - .setView(message) - .setPositiveButton( - android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - callback.accept(ConfirmationCallback.CONFIRM); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .create(); - - alertDialog.setOnShowListener( - (DialogInterface) -> { - Button positive = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - positive.setFocusable(true); - positive.requestFocus(); - }); - alertDialog.show(); - } - - @Override - public void showFileOperationStatus(@Status int status, @OpType int opType, - int docCount) { + public void showFileOperationStatus(@Status int status, @OpType int opType, int docCount) { if (status == FileOperations.Callback.STATUS_REJECTED) { showOperationUnsupported(); return; @@ -143,9 +102,6 @@ public interface DialogController { case FileOperationService.OPERATION_EXTRACT: Snackbars.showExtract(mActivity, docCount); break; - case FileOperationService.OPERATION_DELETE: - Snackbars.showDelete(mActivity, docCount); - break; default: throw new UnsupportedOperationException("Unsupported Operation: " + opType); } diff --git a/src/com/android/documentsui/ui/Snackbars.java b/src/com/android/documentsui/ui/Snackbars.java index 005e3ef32..c587e0053 100644 --- a/src/com/android/documentsui/ui/Snackbars.java +++ b/src/com/android/documentsui/ui/Snackbars.java @@ -26,7 +26,11 @@ import android.widget.TextView; import com.android.documentsui.R; import com.android.documentsui.base.Shared; +import java.util.function.Consumer; + public final class Snackbars { + public static final int DELETION_TIMEOUT = 10000; + private Snackbars() {} public static final void showDocumentsClipped(Activity activity, int docCount) { @@ -55,9 +59,14 @@ public final class Snackbars { makeSnackbar(activity, message, Snackbar.LENGTH_SHORT).show(); } - public static final void showDelete(Activity activity, int docCount) { + public static final Snackbar showDelete(Activity activity, int docCount, Consumer<View> action, + Snackbar.Callback callback) { CharSequence message = Shared.getQuantityString(activity, R.plurals.deleting, docCount); - makeSnackbar(activity, message, Snackbar.LENGTH_SHORT).show(); + CharSequence actionText = activity.getResources().getText(R.string.undo); + Snackbar snackbar = makeSnackbarWithAction(activity, docCount, message, DELETION_TIMEOUT, + actionText, action, callback); + snackbar.show(); + return snackbar; } public static final void showOperationRejected(Activity activity) { @@ -73,7 +82,6 @@ public final class Snackbars { } public static final void showInspectorError(Activity activity) { - //Document Inspector uses a different view from other files app activities. final View view = activity.findViewById(R.id.fragment_container); Snackbar.make(view, R.string.inspector_load_error, Snackbar.LENGTH_INDEFINITE).show(); @@ -90,6 +98,14 @@ public final class Snackbars { snackbar.show(); } + public static final Snackbar makeSnackbarWithAction(Activity activity, int docCount, + CharSequence message, int duration, CharSequence actionText, + Consumer<View> action, final Snackbar.Callback callback) { + return makeSnackbar(activity, message, duration) + .setAction(actionText, action::accept) + .addCallback(callback); + } + public static final Snackbar makeSnackbar(Activity activity, @StringRes int messageId, int duration) { return Snackbars.makeSnackbar( diff --git a/tests/common/com/android/documentsui/TestActionModeAddons.java b/tests/common/com/android/documentsui/TestActionModeAddons.java index 5379bbb4c..c928d6f23 100644 --- a/tests/common/com/android/documentsui/TestActionModeAddons.java +++ b/tests/common/com/android/documentsui/TestActionModeAddons.java @@ -15,20 +15,12 @@ */ package com.android.documentsui; -import com.android.documentsui.testing.TestConfirmationCallback; - public class TestActionModeAddons implements ActionModeAddons { public boolean finishActionModeCalled; - public final TestConfirmationCallback finishOnConfirmed = new TestConfirmationCallback(); @Override public void finishActionMode() { finishActionModeCalled = true; } - - @Override - public void finishOnConfirmed(int code) { - finishOnConfirmed.accept(code); - } } diff --git a/tests/common/com/android/documentsui/bots/DirectoryListBot.java b/tests/common/com/android/documentsui/bots/DirectoryListBot.java index aaf5c8c79..dd8688360 100644 --- a/tests/common/com/android/documentsui/bots/DirectoryListBot.java +++ b/tests/common/com/android/documentsui/bots/DirectoryListBot.java @@ -56,7 +56,7 @@ public class DirectoryListBot extends Bots.BaseBot { private static final String DIR_LIST_ID = "com.android.documentsui:id/dir_list"; private static final BySelector SNACK_DELETE = - By.desc(Pattern.compile("^Deleting [0-9]+ file.+")); + By.text(Pattern.compile("^Deleting [0-9]+ item.+")); private UiAutomation mAutomation; public DirectoryListBot( @@ -222,6 +222,12 @@ public class DirectoryListBot extends Bots.BaseBot { return mDevice.wait(Until.findObject(By.text(message)), mTimeout); } + public void clickSnackbarAction() throws UiObjectNotFoundException { + UiObject snackbarAction = + findObject("com.android.documentsui:id/snackbar_action"); + snackbarAction.click(); + } + public void waitForDeleteSnackbar() { mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout); } diff --git a/tests/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java index b67ce46dd..d87cd0c80 100644 --- a/tests/common/com/android/documentsui/ui/TestDialogController.java +++ b/tests/common/com/android/documentsui/ui/TestDialogController.java @@ -17,18 +17,15 @@ package com.android.documentsui.ui; import android.app.FragmentManager; -import com.android.documentsui.base.ConfirmationCallback; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperations; import junit.framework.Assert; -import java.util.List; public class TestDialogController implements DialogController { - public int mNextConfirmationCode; private int mFileOpStatus; private boolean mNoApplicationFound; private boolean mDocumentsClipped; @@ -37,13 +34,6 @@ public class TestDialogController implements DialogController { private DocumentInfo mOverwriteTarget; public TestDialogController() { - // by default, always confirm - mNextConfirmationCode = ConfirmationCallback.CONFIRM; - } - - @Override - public void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback) { - callback.accept(mNextConfirmationCode); } @Override @@ -107,12 +97,4 @@ public class TestDialogController implements DialogController { public void assertOverwriteConfirmed(DocumentInfo expected) { Assert.assertEquals(expected, mOverwriteTarget); } - - public void confirmNext() { - mNextConfirmationCode = ConfirmationCallback.CONFIRM; - } - - public void rejectNext() { - mNextConfirmationCode = ConfirmationCallback.REJECT; - } } diff --git a/tests/functional/com/android/documentsui/FileDeleteUiTest.java b/tests/functional/com/android/documentsui/FileDeleteUiTest.java index fbeaf3463..b86e417ac 100644 --- a/tests/functional/com/android/documentsui/FileDeleteUiTest.java +++ b/tests/functional/com/android/documentsui/FileDeleteUiTest.java @@ -155,16 +155,16 @@ public class FileDeleteUiTest extends ActivityTest<FilesActivity> { exec.shutdown(); } - public void testDeleteAllDocument() throws Exception { + public void testDeleteAllDocument_AfterSnackbarDismissed() throws Exception { bots.roots.openRoot(ROOT_0_ID); bots.main.clickToolbarOverflowItem( context.getResources().getString(R.string.menu_select_all)); device.waitForIdle(); bots.main.clickToolbarItem(R.id.action_menu_delete); - bots.main.clickDialogOkButton(); device.waitForIdle(); + bots.directory.waitForDeleteSnackbarGone(); try { mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS); } catch (Exception e) { @@ -179,4 +179,18 @@ public class FileDeleteUiTest extends ActivityTest<FilesActivity> { List<DocumentInfo> root1 = mDocsHelper.listChildren(rootDir0.documentId, 1000); assertTrue("Delete operation was not completed", root1.size() == 0); } + + public void testDeleteAllDocument_BeforeSnackbarDismissed() throws Exception { + bots.roots.openRoot(ROOT_0_ID); + bots.main.clickToolbarOverflowItem( + context.getResources().getString(R.string.menu_select_all)); + device.waitForIdle(); + + bots.main.clickToolbarItem(R.id.action_menu_delete); + device.waitForIdle(); + + bots.directory.waitForDeleteSnackbar(); + List<DocumentInfo> root1 = mDocsHelper.listChildren(rootDir0.documentId, 1000); + assertTrue("Documents are deleted", root1.size() == 1000); + } } diff --git a/tests/functional/com/android/documentsui/FileUndoDeletionUiTest.java b/tests/functional/com/android/documentsui/FileUndoDeletionUiTest.java new file mode 100644 index 000000000..0da36ea55 --- /dev/null +++ b/tests/functional/com/android/documentsui/FileUndoDeletionUiTest.java @@ -0,0 +1,99 @@ +/* + * 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.documentsui; + +import static com.android.documentsui.StubProvider.ROOT_0_ID; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.provider.DocumentsContract; +import android.support.test.filters.LargeTest; +import android.support.test.uiautomator.UiObject; +import android.util.Log; + +import com.android.documentsui.base.DocumentInfo; +import com.android.documentsui.files.FilesActivity; +import com.android.documentsui.services.TestNotificationService; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** +* This class test the below points +* - Undo the deleted files +*/ +@LargeTest +public class FileUndoDeletionUiTest extends ActivityTest<FilesActivity> { + private static final String TAG = "FileUndoDeletionUiTest"; + + private static final int DUMMY_FILE_COUNT = 3; + + private String[] filenames = new String[DUMMY_FILE_COUNT]; + + public FileUndoDeletionUiTest() { + super(FilesActivity.class); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + // Set a flag to prevent many refreshes. + Bundle bundle = new Bundle(); + bundle.putBoolean(StubProvider.EXTRA_ENABLE_ROOT_NOTIFICATION, false); + mDocsHelper.configure(null, bundle); + + initTestFiles(); + } + + @Override + public void initTestFiles() { + for (int i = 0; i < DUMMY_FILE_COUNT; i++) { + filenames[i] = "file" + i + ".log"; + mDocsHelper.createDocument(rootDir0, "text/plain", filenames[i]); + } + } + + public void testDeleteUndoDocumentsUI() throws Exception { + bots.roots.openRoot(ROOT_0_ID); + bots.main.clickToolbarOverflowItem( + context.getResources().getString(R.string.menu_select_all)); + device.waitForIdle(); + + bots.main.clickToolbarItem(R.id.action_menu_delete); + device.waitForIdle(); + + bots.directory.waitForDeleteSnackbar(); + for (String filename : filenames) { + assertFalse("files are not deleted", bots.directory.hasDocuments(filename)); + } + + bots.directory.clickSnackbarAction(); + device.waitForIdle(); + + assertTrue("deleted files are not restored", bots.directory.hasDocuments(filenames)); + } +} diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java index 1916924ed..00ca81bf0 100644 --- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java @@ -29,6 +29,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; import android.app.Activity; import android.app.PendingIntent; @@ -38,11 +42,13 @@ import android.net.Uri; import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Path; +import android.support.design.widget.Snackbar; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; import android.util.Pair; import android.view.DragEvent; +import android.view.View; import com.android.documentsui.AbstractActionHandler; import com.android.documentsui.R; @@ -68,8 +74,11 @@ import androidx.core.util.Preconditions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.Arrays; +import java.util.function.Consumer; @RunWith(AndroidJUnit4.class) @MediumTest @@ -99,9 +108,8 @@ public class ActionHandlerTest { ((TestActivityConfig) mEnv.injector.config).nextDocumentEnabled = true; mEnv.injector.dialogs = mDialogs; - mHandler = createHandler(); - - mDialogs.confirmNext(); + ActionHandler<TestActivity> handler = createHandler(); + mHandler = spy(handler); mEnv.selectDocument(TestEnv.FILE_GIF); } @@ -161,31 +169,38 @@ public class ActionHandlerTest { mEnv.selectionMgr.clearSelection(); mHandler.deleteSelectedDocuments(); - mDialogs.assertNoFileFailures(); mActivity.startService.assertNotCalled(); - mActionModeAddons.finishOnConfirmed.assertNeverCalled(); } @Test - public void testDeleteSelectedDocuments_Cancelable() { + public void testDeleteSelectedDocuments_Undo() { mEnv.populateStack(); - - mDialogs.rejectNext(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Consumer<View> callback = invocation.getArgument(2); + callback.accept(null); + return null; + } + }).when(mHandler).showDeletionSnackbar(anyObject(), anyInt(), anyObject(), anyObject()); mHandler.deleteSelectedDocuments(); - mDialogs.assertNoFileFailures(); mActivity.startService.assertNotCalled(); - mActionModeAddons.finishOnConfirmed.assertRejected(); } // Recents root means when deleting the srcParent will be null. @Test public void testDeleteSelectedDocuments_RecentsRoot() { mEnv.state.stack.changeRoot(TestProvidersAccess.RECENTS); - + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Snackbar.Callback callback = invocation.getArgument(3); + callback.onDismissed(null, Snackbar.Callback.DISMISS_EVENT_MANUAL); + return null; + } + }).when(mHandler).showDeletionSnackbar(anyObject(), anyInt(), anyObject(), anyObject()); mHandler.deleteSelectedDocuments(); - mDialogs.assertNoFileFailures(); mActivity.startService.assertCalled(); - mActionModeAddons.finishOnConfirmed.assertCalled(); } @Test diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java index 5d4156558..99947e886 100644 --- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java @@ -78,8 +78,6 @@ public class ActionHandlerTest { mLastAccessed ); - mEnv.dialogs.confirmNext(); - mEnv.selectionMgr.select("1"); AsyncTask.setDefaultExecutor(mEnv.mExecutor); |