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

Commit b09f6012 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add a confirmation dialog before overwriting a file in picker." into arc-apps

parents 8739b373 23ac60cd
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -336,4 +336,7 @@

    <!-- Text shown on button to open an application -->
    <string name="open_app">Open <xliff:g id="name" example="Cloud Storage">%1$s</xliff:g></string>

    <!-- Dialog text shown when confirming if they want to overwrite a file -->
    <string name="overwrite_file_confirmation_message">Overwrite <xliff:g id="name" example="foobar.txt">%1$s</xliff:g>?</string>
</resources>
+0 −9
Original line number Diff line number Diff line
@@ -545,15 +545,6 @@ public abstract class BaseActivity
        return mState.stack.peek();
    }

    public Executor getExecutorForCurrentDirectory() {
        final DocumentInfo cwd = getCurrentDirectory();
        if (cwd != null && cwd.authority != null) {
            return ProviderExecutor.forAuthority(cwd.authority);
        } else {
            return AsyncTask.THREAD_POOL_EXECUTOR;
        }
    }

    @Override
    public void onBackPressed() {
        // While action bar is expanded, the state stack UI is hidden.
+124 −6
Original line number Diff line number Diff line
@@ -17,11 +17,19 @@
package com.android.documentsui.picker;

import static com.android.documentsui.base.Shared.DEBUG;
import static com.android.documentsui.base.State.ACTION_CREATE;
import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
import static com.android.documentsui.base.State.ACTION_OPEN_TREE;
import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;

import android.app.Activity;
import android.app.FragmentManager;
import android.content.ClipData;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.Settings;
import android.util.Log;
@@ -31,6 +39,7 @@ import com.android.documentsui.ActivityConfig;
import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.Injector;
import com.android.documentsui.Metrics;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.Features;
@@ -44,8 +53,10 @@ import com.android.documentsui.Model;
import com.android.documentsui.picker.ActionHandler.Addons;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.roots.RootsAccess;
import com.android.documentsui.services.FileOperationService;
import com.android.internal.annotations.VisibleForTesting;

import java.util.Arrays;
import java.util.concurrent.Executor;

import javax.annotation.Nullable;
@@ -59,7 +70,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T

    private final Features mFeatures;
    private final ActivityConfig mConfig;
    private @Nullable Model mModel;
    private final Model mModel;
    private final LastAccessedStorage mLastAccessed;

    ActionHandler(
            T activity,
@@ -68,13 +80,15 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T
            DocumentsAccess docs,
            SearchViewManager searchMgr,
            Lookup<String, Executor> executors,
            Injector injector) {
            Injector injector,
            LastAccessedStorage lastAccessed) {

        super(activity, state, roots, docs, searchMgr, executors, injector);

        mConfig = injector.config;
        mFeatures = injector.features;
        mModel = injector.getModel();
        mLastAccessed = lastAccessed;
    }

    @Override
@@ -136,7 +150,8 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T

    private void loadLastAccessedStack() {
        if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
        new LoadLastAccessedStackTask<>(mActivity, mState, mRoots, this::onLastAccessedStackLoaded)
        new LoadLastAccessedStackTask<>(
                mActivity, mLastAccessed, mState, mRoots, this::onLastAccessedStackLoaded)
                .execute();
    }

@@ -152,13 +167,13 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T

    private void loadDefaultLocation() {
        switch (mState.action) {
            case State.ACTION_PICK_COPY_DESTINATION:
            case ACTION_PICK_COPY_DESTINATION:
            case State.ACTION_CREATE:
                loadHomeDir();
                break;
            case State.ACTION_GET_CONTENT:
            case ACTION_GET_CONTENT:
            case State.ACTION_OPEN:
            case State.ACTION_OPEN_TREE:
            case ACTION_OPEN_TREE:
                mState.stack.changeRoot(mRoots.getRecentsRoot());
                mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
                break;
@@ -221,6 +236,109 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T
        return false;
    }

    void pickDocument(DocumentInfo pickTarget) {
        assert(pickTarget != null);
        Uri result;
        switch (mState.action) {
            case ACTION_OPEN_TREE:
                result = DocumentsContract.buildTreeDocumentUri(
                        pickTarget.authority, pickTarget.documentId);
                break;
            case ACTION_PICK_COPY_DESTINATION:
                result = pickTarget.derivedUri;
                break;
            default:
                // Should not be reached
                throw new IllegalStateException("Invalid mState.action");
        }
        finishPicking(result);
    }

    void saveDocument(
            String mimeType, String displayName, BooleanConsumer inProgressStateListener) {
        assert(mState.action == ACTION_CREATE);
        new CreatePickedDocumentTask(
                mActivity,
                mLastAccessed,
                mState.stack,
                mimeType,
                displayName,
                inProgressStateListener,
                this::onPickFinished)
                .executeOnExecutor(getExecutorForCurrentDirectory());
    }

    // User requested to overwrite a target. If confirmed by user #finishPicking() will be
    // called.
    void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) {
        assert(mState.action == ACTION_CREATE);
        assert(replaceTarget != null);

        mInjector.dialogs.confirmOverwrite(fm, replaceTarget);
    }

    void finishPicking(Uri... docs) {
        new SetLastAccessedStackTask(
                mActivity,
                mLastAccessed,
                mState.stack,
                () -> {
                    onPickFinished(docs);
                }
        ) .executeOnExecutor(getExecutorForCurrentDirectory());
    }

    private void onPickFinished(Uri... uris) {
        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));

        final Intent intent = new Intent();
        if (uris.length == 1) {
            intent.setData(uris[0]);
        } else if (uris.length > 1) {
            final ClipData clipData = new ClipData(
                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
            for (int i = 1; i < uris.length; i++) {
                clipData.addItem(new ClipData.Item(uris[i]));
            }
            intent.setClipData(clipData);
        }

        // TODO: Separate this piece of logic per action.
        // We don't instantiate different objects for different actions at the first place, so it's
        // not a easy task to separate this logic cleanly.
        // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
        // inheritance structure.
        if (mState.action == ACTION_GET_CONTENT) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else if (mState.action == ACTION_OPEN_TREE) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
            // Picking a copy destination is only used internally by us, so we
            // don't need to extend permissions to the caller.
            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
            intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
        } else {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        }

        mActivity.setResult(Activity.RESULT_OK, intent);
        mActivity.finish();
    }

    private Executor getExecutorForCurrentDirectory() {
        final DocumentInfo cwd = mActivity.getCurrentDirectory();
        if (cwd != null && cwd.authority != null) {
            return mExecutors.lookup(cwd.authority);
        } else {
            return AsyncTask.THREAD_POOL_EXECUTOR;
        }
    }

    public interface Addons extends CommonAddons {
        void onAppPicked(ResolveInfo info);
        void onDocumentPicked(DocumentInfo doc);
+108 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.picker;

import android.app.Activity;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.support.design.widget.Snackbar;
import android.util.Log;

import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.R;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.PairedTask;
import com.android.documentsui.ui.Snackbars;

import java.util.function.Consumer;

/**
 * Task that creates a new document in the background.
 */
class CreatePickedDocumentTask extends PairedTask<Activity, Void, Uri> {
    private static final String TAG = "CreatePickedDocumentTas";

    private final LastAccessedStorage mLastAccessed;
    private final DocumentStack mStack;
    private final String mMimeType;
    private final String mDisplayName;
    private final BooleanConsumer mInProgressStateListener;
    private final Consumer<Uri> mCallback;

    CreatePickedDocumentTask(
            Activity activity,
            LastAccessedStorage lastAccessed,
            DocumentStack stack,
            String mimeType,
            String displayName,
            BooleanConsumer inProgressStateListener,
            Consumer<Uri> callback) {
        super(activity);
        mLastAccessed = lastAccessed;
        mStack = stack;
        mMimeType = mimeType;
        mDisplayName = displayName;
        mInProgressStateListener = inProgressStateListener;
        mCallback = callback;
    }

    @Override
    protected void prepare() {
        mInProgressStateListener.accept(true);
    }

    @Override
    protected Uri run(Void... params) {
        DocumentInfo cwd = mStack.peek();

        final ContentResolver resolver = mOwner.getContentResolver();
        ContentProviderClient client = null;
        Uri childUri = null;
        try {
            client = DocumentsApplication.acquireUnstableProviderOrThrow(
                    resolver, cwd.derivedUri.getAuthority());
            childUri = DocumentsContract.createDocument(
                    client, cwd.derivedUri, mMimeType, mDisplayName);
        } catch (Exception e) {
            Log.w(TAG, "Failed to create document", e);
        } finally {
            ContentProviderClient.releaseQuietly(client);
        }

        if (childUri != null) {
            mLastAccessed.setLastAccessed(mOwner, mStack);
        }

        return childUri;
    }

    @Override
    protected void finish(Uri result) {
        if (result != null) {
            mCallback.accept(result);
        } else {
            Snackbars.makeSnackbar(
                    mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show();
        }

        mInProgressStateListener.accept(false);
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.documentsui.picker;

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

import android.app.Activity;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -114,7 +115,12 @@ public class LastAccessedProvider extends ContentProvider {
        }
    }

    public static void setLastAccessed(
    /**
     * Rather than concretely depending on LastAccessedProvider, consider using
     * {@link LastAccessedStorage#setLastAccessed(Activity, DocumentStack)}.
     */
    @Deprecated
    static void setLastAccessed(
            ContentResolver resolver, String packageName, DocumentStack stack) {
        final ContentValues values = new ContentValues();
        final byte[] rawStack = DurableUtils.writeToArrayOrNull(stack);
Loading