Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 65bbe793 authored by shawnlin's avatar shawnlin
Browse files

Fixed files flickering in & out during a deletion.

Clear selection when all ids in it are either no longer in the current cursor or in the failure list.

Change-Id: Idc1de86f8173626765236e0bf6bd40d421cef51b
Fixes: 112741407
Test: manul, add some files, delete files, observe the deletion process
Test: atest DocumentsUITests
parent bda5932d
Loading
Loading
Loading
Loading
+46 −19
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import static com.android.documentsui.base.SharedMinimal.VERBOSE;
import androidx.annotation.IntDef;
import android.app.AuthenticationRequiredException;
import android.database.Cursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
@@ -38,7 +37,6 @@ import com.android.documentsui.base.DocumentFilters;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Features;
import com.android.documentsui.roots.RootCursorWrapper;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -75,6 +73,7 @@ public class Model {
    private int mCursorCount;
    private String mIds[] = new String[0];
    private Set<Selection<String>> mDocumentsToBeDeleted = new HashSet<>();
    private HashMap<Integer, ArrayList<String>> mDeletionFailedDocIds = new HashMap<>();

    public Model(Features features) {
        mFeatures = features;
@@ -112,6 +111,7 @@ public class Model {
        mIsLoading = false;
        mFileNames.clear();
        mDocumentsToBeDeleted.clear();
        mDeletionFailedDocIds.clear();
        notifyUpdateListeners();
    }

@@ -152,7 +152,7 @@ public class Model {
        notifyUpdateListeners();
    }

    public void restoreDocumentsToBeDeleted(Selection<String> selection) {
    public void clearDocumentsToBeDeleted(Selection<String> selection) {
        if (!mDocumentsToBeDeleted.contains(selection)) {
            return;
        }
@@ -161,27 +161,60 @@ public class Model {
        notifyUpdateListeners();
    }

    private boolean isDocumentToBeDeleted(String id) {
        for (Selection<String> s : mDocumentsToBeDeleted) {
            if (s.contains(id)) {
                return true;
            }
    public void setDeletionFailedUris(Selection<String> selection,
            ArrayList<Uri> deletionFailedUris) {
        if (!mDocumentsToBeDeleted.contains(selection)) {
            return;
        }
        return false;

        mDeletionFailedDocIds.put(selection.hashCode(), ModelId.build(deletionFailedUris));
        updateModelData();
        notifyUpdateListeners();
    }

    private void updateDocumentsToBeDeleted() {
        for (Iterator<Selection<String>> i = mDocumentsToBeDeleted.iterator(); i.hasNext();) {
            Selection<String> selection = i.next();
            int size = selection.size();
            ArrayList<String> failedDocIds = mDeletionFailedDocIds.get(selection.hashCode());
            for (String id : selection) {
                if (!mPositions.containsKey(id)) {
                // Check whether the id is in the current cursor or in the deletion failed list.
                // If all ids are either not in the current cursor or in the deletion failed list,
                // it means the deletion of this selection is done, and we can clear this selection.
                if (!mPositions.containsKey(id) ||
                        (failedDocIds != null && failedDocIds.contains(id))) {
                    size--;
                }
                if (size == 0) {
                    i.remove();
                    mDeletionFailedDocIds.remove(selection.hashCode());
                    break;
                }
            }
        }
    }

    private int getVisibleCount() {
        int count = mPositions.size();
        for (Selection<String> selection : mDocumentsToBeDeleted) {
            for (String id : selection) {
                if (mPositions.containsKey(id)) {
                    count--;
                }
            }
        }
        return count;
    }

    private boolean isDocumentToBeDeleted(String id) {
        for (Selection<String> s : mDocumentsToBeDeleted) {
            if (s.contains(id)) {
                return true;
            }
        }
        return false;
    }

    private int getDocumentsToBeDeletedCount() {
        int count = 0;
        for (Selection<String> s : mDocumentsToBeDeleted) {
@@ -211,21 +244,15 @@ public class Model {
            }
            // Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
            // unique string that can be used to identify the document referred to by the cursor.
            // If the cursor is a merged cursor over multiple authorities, then prefix the ids
            // with the authority to avoid collisions.
            if (mCursor instanceof MergeCursor) {
                tmpIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
                        + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
            } else {
                tmpIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
            }
            // Prefix the ids with the authority to avoid collisions.
            tmpIds[pos] = ModelId.build(mCursor);
            mPositions.put(tmpIds[pos], pos);
            mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
        }

        updateDocumentsToBeDeleted();

        mIds = new String[mCursorCount - getDocumentsToBeDeletedCount()];
        mIds = new String[getVisibleCount()];
        int index = 0;
        for (int i = 0; i < mCursorCount; ++i) {
            if (!isDocumentToBeDeleted(tmpIds[i])) {
+68 −0
Original line number Diff line number Diff line
package com.android.documentsui;

import static com.android.documentsui.base.DocumentInfo.getCursorString;

import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.util.Log;

import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.roots.RootCursorWrapper;

import java.util.ArrayList;
import java.util.List;

public class ModelId {
    private final static String TAG = "ModelId";

    public static final String build(Uri uri) {
        String documentId;
        try {
            documentId = DocumentsContract.getDocumentId(uri);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Failed to get document id.", e);
            return null;
        }
        String authority;
        authority = uri.getAuthority();
        return ModelId.build(authority, documentId);
    }

    public static final String build(DocumentInfo docInfo) {
        if (docInfo == null) {
            return null;
        }
        return ModelId.build(docInfo.authority, docInfo.documentId);
    }

    public static final String build(Cursor cursor) {
        if (cursor == null) {
            return null;
        }
        return ModelId.build(getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY),
                getCursorString(cursor, DocumentsContract.Document.COLUMN_DOCUMENT_ID));
    }

    public static final ArrayList<String> build(ArrayList<Uri> uris) {
        if (uris == null || uris.isEmpty()) {
            return null;
        }
        ArrayList<String> ids = new ArrayList<>();
        String id;
        for (Uri uri : uris) {
            id = ModelId.build(uri);
            if (id != null) {
                ids.add(id);
            }
        }
        return ids;
    }

    public static final String build(String authority, String docId) {
        if (authority == null || authority.isEmpty() || docId == null || docId.isEmpty()) {
            return null;
        }
        return authority + "|" + docId;
    }
}
+30 −5
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.Log;
@@ -64,6 +66,7 @@ import com.android.documentsui.files.ActionHandler.Addons;
import com.android.documentsui.inspector.InspectorActivity;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.services.DeleteJob;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperations;
@@ -298,7 +301,7 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
    @Override
    public void deleteSelectedDocuments() {
        Metrics.logUserAction(mActivity, Metrics.USER_ACTION_DELETE);
        Selection<String> selection = getSelectedOrFocused();
        final Selection<String> selection = getSelectedOrFocused();

        if (selection.isEmpty()) {
            return;
@@ -323,7 +326,7 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
        mModel.markDocumentsToBeDeleted(selection);
        Consumer<View> action = v -> {
            Metrics.logUserAction(mActivity, Metrics.USER_ACTION_UNDO_DELETE);
            mModel.restoreDocumentsToBeDeleted(selection);
            mModel.clearDocumentsToBeDeleted(selection);
        };
        Snackbar.Callback callback = new Snackbar.Callback() {
            @Override
@@ -336,9 +339,31 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
                            .withSrcs(srcs)
                            .withSrcParent(srcParent == null ? null : srcParent.derivedUri)
                            .build();
                    operation.addMessageListener(new Handler.Callback() {
                        @Override
                        public boolean handleMessage(Message message) {
                            if (message.what == FileOperationService.MESSAGE_FINISH) {
                                operation.removeMessageListener(this);

                                // If failure count equals selection size,
                                // it means all deletions failed. Just clear the selection.
                                final int failureCount = message.arg1;
                                if (failureCount == selection.size()) {
                                    mModel.clearDocumentsToBeDeleted(selection);
                                    return true;
                                }

                    FileOperations.start(mActivity, operation, null,
                            FileOperations.createJobId());
                                ArrayList<Uri> failureUris = message.getData()
                                        .getParcelableArrayList(DeleteJob.KEY_FAILED_URIS);
                                if (failureUris != null) {
                                    mModel.setDeletionFailedUris(selection, failureUris);
                                }
                                return true;
                            }
                            return false;
                        }
                    });
                    FileOperations.start(mActivity, operation, null, FileOperations.createJobId());
                }
                if (mDeletionSnackbar == snackbar) {
                    mDeletionSnackbar = null;
+37 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.documentsui.services;

import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.services.FileOperationService.MESSAGE_FINISH;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;

import android.app.Notification;
@@ -24,6 +25,10 @@ import android.app.Notification.Builder;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

import com.android.documentsui.Metrics;
@@ -34,16 +39,22 @@ import com.android.documentsui.base.Features;
import com.android.documentsui.clipping.UrisSupplier;

import java.io.FileNotFoundException;
import java.util.ArrayList;

import javax.annotation.Nullable;

final class DeleteJob extends ResolvedResourcesJob {
public final class DeleteJob extends ResolvedResourcesJob {

    private static final String TAG = "DeleteJob";

    public final static String KEY_FAILED_URIS = "deletion_failed_uris";

    private final Uri mParentUri;

    private volatile int mDocsProcessed = 0;

    private final Messenger mMessenger;

    /**
     * Moves files to a destination identified by {@code destination}.
     * Performs most work by delegating to CopyJob, then deleting
@@ -52,9 +63,10 @@ final class DeleteJob extends ResolvedResourcesJob {
     * @see @link {@link Job} constructor for most param descriptions.
     */
    DeleteJob(Context service, Listener listener, String id, DocumentStack stack,
            UrisSupplier srcs, @Nullable Uri srcParent, Features features) {
            UrisSupplier srcs, Messenger messenger, @Nullable Uri srcParent, Features features) {
        super(service, listener, id, OPERATION_DELETE, stack, srcs, features);
        mParentUri = srcParent;
        mMessenger = messenger;
    }

    @Override
@@ -129,6 +141,29 @@ final class DeleteJob extends ResolvedResourcesJob {
        Metrics.logFileOperation(service, operationType, mResolvedDocs, null);
    }

    @Override
    void finish() {
        super.finish();
        try {
            Message message = Message.obtain();
            message.what = MESSAGE_FINISH;
            message.arg1 = failureCount;
            if (failureCount > 0 && failureCount < mResourceUris.getItemCount()) {
                Bundle b = new Bundle();
                ArrayList<Uri> uris = new ArrayList<>();
                uris.addAll(failedUris);
                for (DocumentInfo documentInfo : failedDocs) {
                    uris.add(documentInfo.derivedUri);
                }
                b.putParcelableArrayList(KEY_FAILED_URIS, uris);
                message.setData(b);
            }
            mMessenger.send(message);
        } catch (RemoteException e) {
            // Ignore. Most likely the frontend was killed.
        }
    }

    @Override
    public String toString() {
        return new StringBuilder()
+1 −1
Original line number Diff line number Diff line
@@ -261,7 +261,7 @@ public abstract class FileOperation implements Parcelable {
                            getMessenger(), features);
                case OPERATION_DELETE:
                    return new DeleteJob(service, listener, id, getDestination(), getSrc(),
                            mSrcParent, features);
                            getMessenger(), mSrcParent, features);
                default:
                    throw new UnsupportedOperationException("Unsupported op type: " + getOpType());
            }
Loading