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

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

Merge "[multi-part] Eliminate 1k selection limit" into nyc-andromeda-dev

parents aee0e623 edce554c
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -113,9 +113,13 @@
            </intent-filter>
        </receiver>

        <!-- Run FileOperationService in a separate process so that we can use FileLock class to
            wait until jumbo clip is done writing to disk before reading it. See ClipStorage for
            details. -->
        <service
            android:name=".services.FileOperationService"
            android:exported="false">
            android:exported="false"
            android:process=":com.android.documentsui.services">
        </service>
    </application>
</manifest>
+343 −0
Original line number 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;

import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_TAG;
import static com.android.documentsui.DocumentClipper.OP_TYPE_KEY;
import static com.android.documentsui.DocumentClipper.SRC_PARENT_KEY;

import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.content.ClipData;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.support.annotation.VisibleForTesting;
import android.util.Log;

import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;

/**
 * ClipDetails is a parcelable project providing information of different type of file
 * management operations like cut, move and copy.
 *
 * Under the hood it provides cross-process synchronization support such that its consumer doesn't
 * need to explicitly synchronize its access.
 */
public abstract class ClipDetails implements Parcelable {
    private final @OpType int mOpType;

    // This field is used only for moving and deleting. Currently it's not the case,
    // but in the future those files may be from multiple different parents. In
    // such case, this needs to be replaced with pairs of parent and child.
    private final @Nullable Uri mSrcParent;

    private ClipDetails(ClipData clipData) {
        PersistableBundle bundle = clipData.getDescription().getExtras();
        mOpType = bundle.getInt(OP_TYPE_KEY);

        String srcParentString = bundle.getString(SRC_PARENT_KEY);
        mSrcParent = (srcParentString == null) ? null : Uri.parse(srcParentString);

        // Only copy doesn't need src parent
        assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
    }

    private ClipDetails(@OpType int opType, @Nullable Uri srcParent) {
        mOpType = opType;
        mSrcParent = srcParent;

        // Only copy doesn't need src parent
        assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
    }

    public @OpType int getOpType() {
        return mOpType;
    }

    public @Nullable Uri getSrcParent() {
        return mSrcParent;
    }

    public abstract int getItemCount();

    /**
     * Gets doc list from this clip detail. This may only be called once because it may read a file
     * to get the list.
     */
    public Iterable<Uri> getDocs(Context context) throws IOException {
        ClipStorage storage = DocumentsApplication.getClipStorage(context);

        return getDocs(storage);
    }

    @VisibleForTesting
    abstract Iterable<Uri> getDocs(ClipStorage storage) throws IOException;

    public void dispose(Context context) {
        ClipStorage storage = DocumentsApplication.getClipStorage(context);
        dispose(storage);
    }

    @VisibleForTesting
    void dispose(ClipStorage storage) {}

    private ClipDetails(Parcel in) {
        mOpType = in.readInt();
        mSrcParent = in.readParcelable(ClassLoader.getSystemClassLoader());
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @CallSuper
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mOpType);
        dest.writeParcelable(mSrcParent, 0);
    }

    private void appendTo(StringBuilder builder) {
        builder.append("opType=").append(mOpType);
        builder.append(", srcParent=").append(mSrcParent);
    }

    public static ClipDetails createClipDetails(ClipData clipData) {
        ClipDetails details;
        PersistableBundle bundle = clipData.getDescription().getExtras();
        if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
            details = new JumboClipDetails(clipData);
        } else {
            details = new StandardClipDetails(clipData);
        }

        return details;
    }

    public static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
            Selection selection, Function<String, Uri> uriBuilder, Context context) {
        ClipStorage storage = DocumentsApplication.getClipStorage(context);

        List<Uri> uris = new ArrayList<>(selection.size());
        for (String id : selection) {
            uris.add(uriBuilder.apply(id));
        }

        return createClipDetails(opType, srcParent, uris, storage);
    }

    @VisibleForTesting
    static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
            List<Uri> uris, ClipStorage storage) {
        ClipDetails details = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
                ? new JumboClipDetails(opType, srcParent, uris, storage)
                : new StandardClipDetails(opType, srcParent, uris);

        return details;
    }

    private static class JumboClipDetails extends ClipDetails {
        private static final String TAG = "JumboClipDetails";

        private final long mSelectionTag;
        private final int mSelectionSize;

        private transient ClipStorage.Reader mReader;

        private JumboClipDetails(ClipData clipData) {
            super(clipData);

            PersistableBundle bundle = clipData.getDescription().getExtras();
            mSelectionTag = bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
            assert(mSelectionTag != ClipStorage.NO_SELECTION_TAG);

            mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE);
            assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
        }

        private JumboClipDetails(@OpType int opType, @Nullable Uri srcParent, Collection<Uri> uris,
                ClipStorage storage) {
            super(opType, srcParent);

            mSelectionTag = storage.createTag();
            new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
            mSelectionSize = uris.size();
        }

        @Override
        public int getItemCount() {
            return mSelectionSize;
        }

        @Override
        public Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
            if (mReader != null) {
                throw new IllegalStateException(
                        "JumboClipDetails#getDocs() can only be called once.");
            }

            mReader = storage.createReader(mSelectionTag);

            return mReader;
        }

        @Override
        void dispose(ClipStorage storage) {
            if (mReader != null) {
                try {
                    mReader.close();
                } catch (IOException e) {
                    Log.w(TAG, "Failed to close the reader.", e);
                }
            }
            try {
                storage.delete(mSelectionTag);
            } catch(IOException e) {
                Log.w(TAG, "Failed to delete clip with tag: " + mSelectionTag + ".", e);
            }
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("JumboClipDetails{");
            super.appendTo(builder);
            builder.append(", selectionTag=").append(mSelectionTag);
            builder.append(", selectionSize=").append(mSelectionSize);
            builder.append("}");
            return builder.toString();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeLong(mSelectionTag);
            dest.writeInt(mSelectionSize);
        }

        private JumboClipDetails(Parcel in) {
            super(in);

            mSelectionTag = in.readLong();
            mSelectionSize = in.readInt();
        }

        public static final Parcelable.Creator<JumboClipDetails> CREATOR =
                new Parcelable.Creator<JumboClipDetails>() {

                    @Override
                    public JumboClipDetails createFromParcel(Parcel source) {
                        return new JumboClipDetails(source);
                    }

                    @Override
                    public JumboClipDetails[] newArray(int size) {
                        return new JumboClipDetails[size];
                    }
                };
    }

    @VisibleForTesting
    public static class StandardClipDetails extends ClipDetails {
        private final List<Uri> mDocs;

        private StandardClipDetails(ClipData clipData) {
            super(clipData);
            mDocs = listDocs(clipData);
        }

        @VisibleForTesting
        public StandardClipDetails(@OpType int opType, @Nullable Uri srcParent, List<Uri> docs) {
            super(opType, srcParent);

            mDocs = docs;
        }

        private List<Uri> listDocs(ClipData clipData) {
            ArrayList<Uri> docs = new ArrayList<>(clipData.getItemCount());

            for (int i = 0; i < clipData.getItemCount(); ++i) {
                Uri uri = clipData.getItemAt(i).getUri();
                assert(uri != null);
                docs.add(uri);
            }

            return docs;
        }

        @Override
        public int getItemCount() {
            return mDocs.size();
        }

        @Override
        public Iterable<Uri> getDocs(ClipStorage storage) {
            return mDocs;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("StandardClipDetails{");
            super.appendTo(builder);
            builder.append(", ").append("docs=").append(mDocs.toString());
            builder.append("}");
            return builder.toString();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);

            dest.writeTypedList(mDocs);
        }

        private StandardClipDetails(Parcel in) {
            super(in);

            mDocs = in.createTypedArrayList(Uri.CREATOR);
        }

        public static final Parcelable.Creator<StandardClipDetails> CREATOR =
                new Parcelable.Creator<StandardClipDetails>() {

                    @Override
                    public StandardClipDetails createFromParcel(Parcel source) {
                        return new StandardClipDetails(source);
                    }

                    @Override
                    public StandardClipDetails[] newArray(int size) {
                        return new StandardClipDetails[size];
                    }
                };
    }
}
+130 −49
Original line number Diff line number Diff line
@@ -17,16 +17,17 @@
package com.android.documentsui;

import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.VisibleForTesting;
import android.util.Log;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.nio.channels.FileLock;
import java.util.Scanner;

/**
 * Provides support for storing lists of documents identified by Uri.
@@ -36,9 +37,10 @@ import java.util.List;
 */
public final class ClipStorage {

    private static final String PRIMARY_SELECTION = "primary-selection.txt";
    private static final String TAG = "ClipStorage";

    private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
    private static final int NO_SELECTION_TAG = -1;
    public static final long NO_SELECTION_TAG = -1;

    private final File mOutDir;

@@ -51,63 +53,119 @@ public final class ClipStorage {
    }

    /**
     * Returns a writer. Callers must...
     * Creates a clip tag.
     *
     * <li>synchronize on the {@link ClipStorage} instance while writing to this writer.
     * <li>closed the write when finished.
     * NOTE: this tag doesn't guarantee perfect uniqueness, but should work well unless user creates
     * clips more than hundreds of times per second.
     */
    public Writer createWriter() throws IOException {
        File primary = new File(mOutDir, PRIMARY_SELECTION);
        return new Writer(new FileOutputStream(primary));
    public long createTag() {
        return System.currentTimeMillis();
    }

    /**
     * Saves primary uri list to persistent storage.
     * @return tag identifying the saved set.
     * Returns a writer. Callers must close the writer when finished.
     */
    public Writer createWriter(long tag) throws IOException {
        File file = toTagFile(tag);
        return new Writer(file);
    }

    @VisibleForTesting
    public long savePrimary() throws IOException {
        File primary = new File(mOutDir, PRIMARY_SELECTION);
    public Reader createReader(long tag) throws IOException {
        File file = toTagFile(tag);
        return new Reader(file);
    }

        if (!primary.exists()) {
            return NO_SELECTION_TAG;
    @VisibleForTesting
    public void delete(long tag) throws IOException {
        toTagFile(tag).delete();
    }

        long tag = System.currentTimeMillis();
        File dest = toTagFile(tag);
        primary.renameTo(dest);
    private File toTagFile(long tag) {
        return new File(mOutDir, String.valueOf(tag));
    }

    /**
     * Provides initialization of the clip data storage directory.
     */
    static File prepareStorage(File cacheDir) {
        File clipDir = getClipDir(cacheDir);
        clipDir.mkdir();

        return tag;
        assert(clipDir.isDirectory());
        return clipDir;
    }

    @VisibleForTesting
    public List<Uri> read(long tag) throws IOException {
        List<Uri> uris = new ArrayList<>();
        File tagFile = toTagFile(tag);
        try (BufferedReader in = new BufferedReader(new FileReader(tagFile))) {
            String line = null;
            while ((line = in.readLine()) != null) {
                uris.add(Uri.parse(line));
    public static boolean hasDocList(long tag) {
        return tag != NO_SELECTION_TAG;
    }

    private static File getClipDir(File cacheDir) {
        return new File(cacheDir, "clippings");
    }
        return uris;

    static final class Reader implements Iterable<Uri>, Closeable {

        private final Scanner mScanner;
        private final FileLock mLock;

        private Reader(File file) throws IOException {
            FileInputStream inStream = new FileInputStream(file);

            // Lock the file here so it won't pass this line until the corresponding writer is done
            // writing.
            mLock = inStream.getChannel().lock(0L, Long.MAX_VALUE, true);

            mScanner = new Scanner(inStream);
        }

    @VisibleForTesting
    public void delete(long tag) throws IOException {
        toTagFile(tag).delete();
        @Override
        public Iterator iterator() {
            return new Iterator(mScanner);
        }

    private File toTagFile(long tag) {
        return new File(mOutDir, String.valueOf(tag));
        @Override
        public void close() throws IOException {
            if (mLock != null) {
                mLock.release();
            }

            if (mScanner != null) {
                mScanner.close();
            }
        }
    }

    private static final class Iterator implements java.util.Iterator {
        private final Scanner mScanner;

        private Iterator(Scanner scanner) {
            mScanner = scanner;
        }

        @Override
        public boolean hasNext() {
            return mScanner.hasNextLine();
        }

        @Override
        public Uri next() {
            String line = mScanner.nextLine();
            return Uri.parse(line);
        }
    }

    public static final class Writer implements Closeable {
    private static final class Writer implements Closeable {

        private final FileOutputStream mOut;
        private final FileLock mLock;

        public Writer(FileOutputStream out) {
            mOut = out;
        private Writer(File file) throws IOException {
            mOut = new FileOutputStream(file);

            // Lock the file here so copy tasks would wait until everything is flushed to disk
            // before start to run.
            mLock = mOut.getChannel().lock();
        }

        public void write(Uri uri) throws IOException {
@@ -117,20 +175,43 @@ public final class ClipStorage {

        @Override
        public void close() throws IOException {
            if (mLock != null) {
                mLock.release();
            }

            if (mOut != null) {
                mOut.close();
            }
        }
    }

    /**
     * Provides initialization and cleanup of the clip data storage directory.
     * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
     */
    static File prepareStorage(File cacheDir) {
        File clipDir = new File(cacheDir, "clippings");
        if (clipDir.exists()) {
            Files.deleteRecursively(clipDir);
    static final class PersistTask extends AsyncTask<Void, Void, Void> {

        private final ClipStorage mClipStorage;
        private final Iterable<Uri> mUris;
        private final long mTag;

        PersistTask(ClipStorage clipStorage, Iterable<Uri> uris, long tag) {
            mClipStorage = clipStorage;
            mUris = uris;
            mTag = tag;
        }

        @Override
        protected Void doInBackground(Void... params) {
            try (ClipStorage.Writer writer = mClipStorage.createWriter(mTag)) {
                for (Uri uri: mUris) {
                    assert(uri != null);
                    writer.write(uri);
                }
            } catch (IOException e) {
                Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
            }

            return null;
        }
        assert(!clipDir.exists());
        clipDir.mkdir();
        return clipDir;
    }
}
+156 −151

File changed.

Preview size limit exceeded, changes collapsed.

+7 −9
Original line number Diff line number Diff line
@@ -28,14 +28,13 @@ import android.net.Uri;
import android.os.RemoteException;
import android.text.format.DateUtils;

import java.io.File;

public class DocumentsApplication extends Application {
    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;

    private RootsCache mRoots;

    private ThumbnailCache mThumbnailCache;
    private ClipStorage mClipStorage;
    private DocumentClipper mClipper;

    public static RootsCache getRootsCache(Context context) {
@@ -62,6 +61,10 @@ public class DocumentsApplication extends Application {
        return ((DocumentsApplication) context.getApplicationContext()).mClipper;
    }

    public static ClipStorage getClipStorage(Context context) {
        return ((DocumentsApplication) context.getApplicationContext()).mClipStorage;
    }

    @Override
    public void onCreate() {
        super.onCreate();
@@ -74,7 +77,8 @@ public class DocumentsApplication extends Application {

        mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);

        mClipper = createClipper(this.getApplicationContext());
        mClipStorage = new ClipStorage(ClipStorage.prepareStorage(getCacheDir()));
        mClipper = new DocumentClipper(this, mClipStorage);

        final IntentFilter packageFilter = new IntentFilter();
        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -89,12 +93,6 @@ public class DocumentsApplication extends Application {
        registerReceiver(mCacheReceiver, localeFilter);
    }

    private static DocumentClipper createClipper(Context context) {
        // prepare storage handles initialization and cleanup of the clip directory.
        File clipDir = ClipStorage.prepareStorage(context.getCacheDir());
        return new DocumentClipper(context, new ClipStorage(clipDir));
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
Loading