Loading AndroidManifest.xml +5 −1 Original line number Diff line number Diff line Loading @@ -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> src/com/android/documentsui/ClipDetails.java 0 → 100644 +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]; } }; } } src/com/android/documentsui/ClipStorage.java +130 −49 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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 { Loading @@ -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; } } src/com/android/documentsui/DocumentClipper.java +156 −151 File changed.Preview size limit exceeded, changes collapsed. Show changes src/com/android/documentsui/DocumentsApplication.java +7 −9 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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(); Loading @@ -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); Loading @@ -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 Loading
AndroidManifest.xml +5 −1 Original line number Diff line number Diff line Loading @@ -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>
src/com/android/documentsui/ClipDetails.java 0 → 100644 +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]; } }; } }
src/com/android/documentsui/ClipStorage.java +130 −49 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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 { Loading @@ -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; } }
src/com/android/documentsui/DocumentClipper.java +156 −151 File changed.Preview size limit exceeded, changes collapsed. Show changes
src/com/android/documentsui/DocumentsApplication.java +7 −9 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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(); Loading @@ -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); Loading @@ -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