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

Commit 97b4be46 authored by Steve McKay's avatar Steve McKay
Browse files

Move delete support into FileOperationService.

Refactor Jobs to work with files from multiple providers.
Don't shut down threadpool until service#onDestroy is called.

Bug: 26696797, 26462789, 26567205, 25162803, 26714663
Change-Id: Id43e8e3dc2294cd07dcd6a3477b19efb298c260f
parent d71f9067
Loading
Loading
Loading
Loading
+8 −1
Original line number Original line Diff line number Diff line
@@ -158,6 +158,8 @@
    <string name="copy_preparing">Preparing for copy\u2026</string>
    <string name="copy_preparing">Preparing for copy\u2026</string>
    <!-- Text shown on the notification while DocumentsUI performs setup in preparation for moving files [CHAR LIMIT=32] -->
    <!-- Text shown on the notification while DocumentsUI performs setup in preparation for moving files [CHAR LIMIT=32] -->
    <string name="move_preparing">Preparing for move\u2026</string>
    <string name="move_preparing">Preparing for move\u2026</string>
    <!-- Text shown on the notification while DocumentsUI performs setup in preparation for deleting files [CHAR LIMIT=32] -->
    <string name="delete_preparing">Preparing for delete\u2026</string>
    <!-- Title of the copy error notification [CHAR LIMIT=48] -->
    <!-- Title of the copy error notification [CHAR LIMIT=48] -->
    <plurals name="copy_error_notification_title">
    <plurals name="copy_error_notification_title">
        <item quantity="one">Couldn\'t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
        <item quantity="one">Couldn\'t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
@@ -168,6 +170,11 @@
        <item quantity="one">Couldn\'t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
        <item quantity="one">Couldn\'t move <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
        <item quantity="other">Couldn\'t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
        <item quantity="other">Couldn\'t move <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
    </plurals>
    </plurals>
    <!-- Title of the delete error notification [CHAR LIMIT=48] -->
    <plurals name="delete_error_notification_title">
        <item quantity="one">Couldn\'t delete <xliff:g id="count" example="1">%1$d</xliff:g> file</item>
        <item quantity="other">Couldn\'t delete <xliff:g id="count" example="2">%1$d</xliff:g> files</item>
    </plurals>
    <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
    <!-- Second line for notifications saying that more information will be shown after touching [CHAR LIMIT=48] -->
    <string name="notification_touch_for_details">Touch to view details</string>
    <string name="notification_touch_for_details">Touch to view details</string>
    <!-- Label of a dialog button for retrying a failed operation [CHAR LIMIT=24] -->
    <!-- Label of a dialog button for retrying a failed operation [CHAR LIMIT=24] -->
+28 −24
Original line number Original line Diff line number Diff line
@@ -124,10 +124,12 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi


    public static final int REQUEST_COPY_DESTINATION = 1;
    public static final int REQUEST_COPY_DESTINATION = 1;


    private static final String TAG = "DirectoryFragment";
    static final boolean DEBUG_ENABLE_DND = true;


    private static final String TAG = "DirectoryFragment";
    private static final int LOADER_ID = 42;
    private static final int LOADER_ID = 42;
    static final boolean DEBUG_ENABLE_DND = true;
    private static final int DELETE_UNDO_TIMEOUT = 5000;
    private static final int DELETE_JOB_DELAY = 5500;


    private static final String EXTRA_TYPE = "type";
    private static final String EXTRA_TYPE = "type";
    private static final String EXTRA_ROOT = "root";
    private static final String EXTRA_ROOT = "root";
@@ -339,7 +341,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
                    : MultiSelectManager.MODE_SINGLE);
                    : MultiSelectManager.MODE_SINGLE);
        mSelectionManager.addCallback(new SelectionModeListener());
        mSelectionManager.addCallback(new SelectionModeListener());


        mModel = new Model(context);
        mModel = new Model();
        mModel.addUpdateListener(mAdapter);
        mModel.addUpdateListener(mAdapter);
        mModel.addUpdateListener(mModelUpdateListener);
        mModel.addUpdateListener(mModelUpdateListener);


@@ -852,16 +854,32 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
    }
    }


    private void deleteDocuments(final Selection selected) {
    private void deleteDocuments(final Selection selected) {
        Context context = getActivity();
        String message = Shared.getQuantityString(context, R.plurals.deleting, selected.size());


            checkArgument(!selected.isEmpty());
            new GetDocumentsTask() {
                @Override
                void onDocumentsReady(List<DocumentInfo> docs) {
                    // Hide the files in the UI.
                    // Hide the files in the UI.
        final SparseArray<String> toDelete = mAdapter.hide(selected.getAll());
                    final SparseArray<String> hidden = mAdapter.hide(selected.getAll());

                    checkState(DELETE_JOB_DELAY > DELETE_UNDO_TIMEOUT);
                    String operationId = FileOperations.delete(
                            getActivity(), docs, getDisplayState().stack,
                            DELETE_JOB_DELAY);
                    showDeleteSnackbar(hidden, operationId);
                }
            }.execute(selected);
    }

    private void showDeleteSnackbar(final SparseArray<String> hidden, final String jobId) {

        Context context = getActivity();
        String message = Shared.getQuantityString(context, R.plurals.deleting, hidden.size());


        // Show a snackbar informing the user that files will be deleted, and give them an option to
        // Show a snackbar informing the user that files will be deleted, and give them an option to
        // cancel.
        // cancel.
        final Activity activity = getActivity();
        final Activity activity = getActivity();
        Snackbars.makeSnackbar(activity, message, Snackbar.LENGTH_LONG)
        Snackbars.makeSnackbar(activity, message, DELETE_UNDO_TIMEOUT)
                .setAction(
                .setAction(
                        R.string.undo,
                        R.string.undo,
                        new View.OnClickListener() {
                        new View.OnClickListener() {
@@ -874,22 +892,8 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
                            public void onDismissed(Snackbar snackbar, int event) {
                            public void onDismissed(Snackbar snackbar, int event) {
                                if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
                                if (event == Snackbar.Callback.DISMISS_EVENT_ACTION) {
                                    // If the delete was cancelled, just unhide the files.
                                    // If the delete was cancelled, just unhide the files.
                                    mAdapter.unhide(toDelete);
                                    FileOperations.cancel(activity, jobId);
                                } else {
                                    mAdapter.unhide(hidden);
                                    // Actually kick off the delete.
                                    mModel.delete(
                                            selected,
                                            new Model.DeletionListener() {
                                                @Override
                                                  public void onError() {
                                                      Snackbars.makeSnackbar(
                                                              activity,
                                                              R.string.toast_failed_delete,
                                                              Snackbar.LENGTH_LONG)
                                                              .show();

                                                  }
                                            });
                                }
                                }
                            }
                            }
                        })
                        })
+0 −95
Original line number Original line Diff line number Diff line
@@ -24,11 +24,7 @@ import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;


import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Bundle;
import android.os.Looper;
import android.os.Looper;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract;
@@ -39,7 +35,6 @@ import android.util.Log;


import com.android.documentsui.BaseActivity.SiblingProvider;
import com.android.documentsui.BaseActivity.SiblingProvider;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.RootCursorWrapper;
import com.android.documentsui.RootCursorWrapper;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentInfo;
@@ -56,7 +51,6 @@ import java.util.Map;
public class Model implements SiblingProvider {
public class Model implements SiblingProvider {
    private static final String TAG = "Model";
    private static final String TAG = "Model";


    private Context mContext;
    private boolean mIsLoading;
    private boolean mIsLoading;
    private List<UpdateListener> mUpdateListeners = new ArrayList<>();
    private List<UpdateListener> mUpdateListeners = new ArrayList<>();
    @Nullable private Cursor mCursor;
    @Nullable private Cursor mCursor;
@@ -73,10 +67,6 @@ public class Model implements SiblingProvider {
    @Nullable String info;
    @Nullable String info;
    @Nullable String error;
    @Nullable String error;


    Model(Context context) {
        mContext = context;
    }

    /**
    /**
     * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique
     * 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.
     * string that can be used to identify the document referred to by the cursor.
@@ -406,91 +396,6 @@ public class Model implements SiblingProvider {
        return mCursor;
        return mCursor;
    }
    }


    public void delete(Selection selected, DeletionListener listener) {
        final ContentResolver resolver = mContext.getContentResolver();
        new DeleteFilesTask(resolver, listener).execute(selected);
    }

    /**
     * A Task which collects the DocumentInfo for documents that have been marked for deletion,
     * and actually deletes them.
     */
    private class DeleteFilesTask extends AsyncTask<Selection, Void, Void> {
        private ContentResolver mResolver;
        private DeletionListener mListener;
        private boolean mHadTrouble;

        /**
         * @param resolver A ContentResolver for performing the actual file deletions.
         * @param errorCallback A Runnable that is executed in the event that one or more errors
         *     occurred while copying files.  Execution will occur on the UI thread.
         */
        public DeleteFilesTask(ContentResolver resolver, DeletionListener listener) {
            mResolver = resolver;
            mListener = listener;
        }

        @Override
        protected Void doInBackground(Selection... selected) {
            List<DocumentInfo> toDelete = null;
            try {
                toDelete = getDocuments(selected[0]);
            } catch (NullPointerException e) {
                Log.w(TAG, "Failed to retrieve documents for delete.");
                mHadTrouble = true;
                return null;
            }

            for (DocumentInfo doc : toDelete) {
                if (!doc.isDeleteSupported()) {
                    Log.w(TAG, doc + " could not be deleted.  Skipping...");
                    mHadTrouble = true;
                    continue;
                }

                ContentProviderClient client = null;
                try {
                    if (DEBUG) Log.d(TAG, "Deleting: " + doc.displayName);
                    client = DocumentsApplication.acquireUnstableProviderOrThrow(
                        mResolver, doc.derivedUri.getAuthority());
                    DocumentsContract.deleteDocument(client, doc.derivedUri);
                } catch (Exception e) {
                    Log.w(TAG, "Failed to delete " + doc, e);
                    mHadTrouble = true;
                } finally {
                    ContentProviderClient.releaseQuietly(client);
                }
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void _) {
            if (mHadTrouble) {
                // TODO show which files failed? b/23720103
                mListener.onError();
                if (DEBUG) Log.d(TAG, "Deletion task completed.  Some deletions failed.");
            } else {
                if (DEBUG) Log.d(TAG, "Deletion task completed successfully.");
            }

            mListener.onCompletion();
        }
    }

    static class DeletionListener {
        /**
         * Called when deletion has completed (regardless of whether an error occurred).
         */
        void onCompletion() {}

        /**
         * Called at the end of a deletion operation that produced one or more errors.
         */
        void onError() {}
    }

    void addUpdateListener(UpdateListener listener) {
    void addUpdateListener(UpdateListener listener) {
        mUpdateListeners.add(listener);
        mUpdateListeners.add(listener);
    }
    }
+76 −87

File changed.

Preview size limit exceeded, changes collapsed.

+96 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2016 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.services;

import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;

import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
import android.os.RemoteException;
import android.util.Log;

import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;

import java.util.List;

final class DeleteJob extends Job {

    private static final String TAG = "DeleteJob";
    private List<DocumentInfo> mSrcs;

    /**
     * Moves files to a destination identified by {@code destination}.
     * Performs most work by delegating to CopyJob, then deleting
     * a file after it has been copied.
     *
     * @see @link {@link Job} constructor for most param descriptions.
     *
     * @param srcs List of files to delete
     */
    DeleteJob(Context service, Context appContext, Listener listener,
            String id, DocumentStack stack, List<DocumentInfo> srcs) {
        super(service, appContext, listener, OPERATION_DELETE, id, stack);
        this.mSrcs = srcs;
    }

    @Override
    Builder createProgressBuilder() {
        return super.createProgressBuilder(
                service.getString(R.string.move_notification_title),
                R.drawable.ic_menu_copy,
                service.getString(android.R.string.cancel),
                R.drawable.ic_cab_cancel);
    }

    @Override
    public Notification getSetupNotification() {
        return getSetupNotification(service.getString(R.string.delete_preparing));
    }

    @Override
    Notification getFailureNotification() {
        return getFailureNotification(
                R.plurals.delete_error_notification_title, R.drawable.ic_menu_delete);
    }

    @Override
    void start() throws RemoteException {
        for (DocumentInfo doc : mSrcs) {
            if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
            if (!deleteDocument(doc)) {
                Log.w(TAG, "Failed to delete document @ " + doc.derivedUri);
                onFileFailed(doc);
            }
        }
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append("DeleteJob")
                .append("{")
                .append("id=" + id)
                .append("srcs=" + mSrcs)
                .append(", location=" + stack)
                .append("}")
                .toString();
    }
}
Loading