Loading src/com/android/documentsui/ClipStorage.java 0 → 100644 +136 −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 android.net.Uri; import android.support.annotation.VisibleForTesting; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Provides support for storing lists of documents identified by Uri. * * <li>Access to this object *must* be synchronized externally. * <li>All calls to this class are I/O intensive and must be wrapped in an AsyncTask. */ public final class ClipStorage { private static final String PRIMARY_SELECTION = "primary-selection.txt"; private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes(); private static final int NO_SELECTION_TAG = -1; private final File mOutDir; /** * @param outDir see {@link #prepareStorage(File)}. */ public ClipStorage(File outDir) { assert(outDir.isDirectory()); mOutDir = outDir; } /** * Returns a writer. Callers must... * * <li>synchronize on the {@link ClipStorage} instance while writing to this writer. * <li>closed the write when finished. */ public Writer createWriter() throws IOException { File primary = new File(mOutDir, PRIMARY_SELECTION); return new Writer(new FileOutputStream(primary)); } /** * Saves primary uri list to persistent storage. * @return tag identifying the saved set. */ @VisibleForTesting public long savePrimary() throws IOException { File primary = new File(mOutDir, PRIMARY_SELECTION); if (!primary.exists()) { return NO_SELECTION_TAG; } long tag = System.currentTimeMillis(); File dest = toTagFile(tag); primary.renameTo(dest); return tag; } @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)); } } return uris; } @VisibleForTesting public void delete(long tag) throws IOException { toTagFile(tag).delete(); } private File toTagFile(long tag) { return new File(mOutDir, String.valueOf(tag)); } public static final class Writer implements Closeable { private final FileOutputStream mOut; public Writer(FileOutputStream out) { mOut = out; } public void write(Uri uri) throws IOException { mOut.write(uri.toString().getBytes()); mOut.write(LINE_SEPARATOR); } @Override public void close() throws IOException { mOut.close(); } } /** * Provides initialization and cleanup of the clip data storage directory. */ static File prepareStorage(File cacheDir) { File clipDir = new File(cacheDir, "clippings"); if (clipDir.exists()) { Files.deleteRecursively(clipDir); } assert(!clipDir.exists()); clipDir.mkdir(); return clipDir; } } src/com/android/documentsui/DocumentClipper.java +119 −57 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.documentsui; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; Loading @@ -27,6 +28,8 @@ import android.provider.DocumentsContract; import android.support.annotation.Nullable; import android.util.Log; import com.android.documentsui.ClipStorage.Writer; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; Loading @@ -34,11 +37,13 @@ import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; import com.android.documentsui.services.FileOperations; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; /** * ClipboardManager wrapper class providing higher level logical Loading @@ -49,12 +54,15 @@ public final class DocumentClipper { private static final String TAG = "DocumentClipper"; private static final String SRC_PARENT_KEY = "srcParent"; private static final String OP_TYPE_KEY = "opType"; private static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size"; private Context mContext; private ClipboardManager mClipboard; private final Context mContext; private final ClipStorage mClipStorage; private final ClipboardManager mClipboard; DocumentClipper(Context context) { DocumentClipper(Context context, ClipStorage storage) { mContext = context; mClipStorage = storage; mClipboard = context.getSystemService(ClipboardManager.class); } Loading @@ -79,13 +87,6 @@ public final class DocumentClipper { return uri != null && DocumentsContract.isDocumentUri(mContext, uri); } /** * Returns details regarding the documents on the primary clipboard */ public ClipDetails getClipDetails() { return getClipDetails(mClipboard.getPrimaryClip()); } public ClipDetails getClipDetails(@Nullable ClipData clipData) { if (clipData == null) { return null; Loading Loading @@ -127,54 +128,108 @@ public final class DocumentClipper { } /** * Returns ClipData representing the list of docs, or null if docs is empty, * or docs cannot be converted. * Returns {@link ClipData} representing the selection, or null if selection is empty, * or cannot be converted. */ public @Nullable ClipData getClipDataForDocuments( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(selection != null); if (selection.isEmpty()) { Log.w(TAG, "Attempting to clip empty selection. Ignoring."); return null; } return (selection.size() > Shared.MAX_DOCS_IN_INTENT) ? createJumboClipData(uriBuilder, selection, opType) : createStandardClipData(uriBuilder, selection, opType); } /** * Returns ClipData representing the selection. */ public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs, @OpType int opType) { private @Nullable ClipData createStandardClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(!selection.isEmpty()); final ContentResolver resolver = mContext.getContentResolver(); final String[] mimeTypes = getMimeTypes(resolver, docs); ClipData clipData = null; for (DocumentInfo doc : docs) { assert(doc != null); assert(doc.derivedUri != null); if (clipData == null) { // TODO: figure out what this string should be. // Currently it is not displayed anywhere in the UI, but this might change. final String clipLabel = ""; clipData = new ClipData(clipLabel, mimeTypes, new ClipData.Item(doc.derivedUri)); final ArrayList<ClipData.Item> clipItems = new ArrayList<>(); final Set<String> clipTypes = new HashSet<>(); PersistableBundle bundle = new PersistableBundle(); bundle.putInt(OP_TYPE_KEY, opType); clipData.getDescription().setExtras(bundle); } else { // TODO: update list of mime types in ClipData. clipData.addItem(new ClipData.Item(doc.derivedUri)); } int clipCount = 0; for (String id : selection) { assert(id != null); Uri uri = uriBuilder.apply(id); if (clipCount <= Shared.MAX_DOCS_IN_INTENT) { DocumentInfo.addMimeTypes(resolver, uri, clipTypes); clipItems.add(new ClipData.Item(uri)); } return clipData; clipCount++; } private static String[] getMimeTypes(ContentResolver resolver, List<DocumentInfo> docs) { final HashSet<String> mimeTypes = new HashSet<>(); for (DocumentInfo doc : docs) { assert(doc != null); assert(doc.derivedUri != null); final Uri uri = doc.derivedUri; if ("content".equals(uri.getScheme())) { mimeTypes.add(resolver.getType(uri)); final String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); if (streamTypes != null) { mimeTypes.addAll(Arrays.asList(streamTypes)); ClipDescription description = new ClipDescription( "", // Currently "label" is not displayed anywhere in the UI. clipTypes.toArray(new String[0])); description.setExtras(bundle); return new ClipData(description, clipItems); } /** * Returns ClipData representing the list of docs, or null if docs is empty, * or docs cannot be converted. */ private @Nullable ClipData createJumboClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(!selection.isEmpty()); final ContentResolver resolver = mContext.getContentResolver(); final ArrayList<ClipData.Item> clipItems = new ArrayList<>(); final Set<String> clipTypes = new HashSet<>(); PersistableBundle bundle = new PersistableBundle(); bundle.putInt(OP_TYPE_KEY, opType); bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size()); int clipCount = 0; synchronized (mClipStorage) { try (Writer writer = mClipStorage.createWriter()) { for (String id : selection) { assert(id != null); Uri uri = uriBuilder.apply(id); if (clipCount <= Shared.MAX_DOCS_IN_INTENT) { DocumentInfo.addMimeTypes(resolver, uri, clipTypes); clipItems.add(new ClipData.Item(uri)); } writer.write(uri); clipCount++; } } catch (IOException e) { Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e); return null; } } return mimeTypes.toArray(new String[0]); ClipDescription description = new ClipDescription( "", // Currently "label" is not displayed anywhere in the UI. clipTypes.toArray(new String[0])); description.setExtras(bundle); return new ClipData(description, clipItems); } /** * Puts {@code ClipData} in a primary clipboard, describing a copy operation */ public void clipDocumentsForCopy(List<DocumentInfo> docs) { ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_COPY); public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) { ClipData data = getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY); assert(data != null); mClipboard.setPrimaryClip(data); Loading @@ -183,24 +238,24 @@ public final class DocumentClipper { /** * Puts {@Code ClipData} in a primary clipboard, describing a cut operation */ public void clipDocumentsForCut(List<DocumentInfo> docs, DocumentInfo srcParent) { assert(docs != null); assert(!docs.isEmpty()); assert(srcParent != null); assert(srcParent.derivedUri != null); public void clipDocumentsForCut( Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) { assert(!selection.isEmpty()); assert(parent.derivedUri != null); ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_MOVE); ClipData data = getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_MOVE); assert(data != null); PersistableBundle bundle = data.getDescription().getExtras(); bundle.putString(SRC_PARENT_KEY, srcParent.derivedUri.toString()); bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString()); mClipboard.setPrimaryClip(data); } private DocumentInfo createDocument(Uri uri) { DocumentInfo doc = null; if (uri != null && DocumentsContract.isDocumentUri(mContext, uri)) { if (isDocumentUri(uri)) { ContentResolver resolver = mContext.getContentResolver(); try { doc = DocumentInfo.fromUri(resolver, uri); Loading @@ -219,8 +274,11 @@ public final class DocumentClipper { * @param docStack the document stack to the destination folder, * @param callback callback to notify when operation finishes. */ public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack, public void copyFromClipboard( DocumentInfo destination, DocumentStack docStack, FileOperations.Callback callback) { copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback); } Loading @@ -232,8 +290,12 @@ public final class DocumentClipper { * @param clipData the clipData to copy from, or null to copy from clipboard * @param callback callback to notify when operation finishes */ public void copyFromClipData(final DocumentInfo destination, DocumentStack docStack, @Nullable final ClipData clipData, final FileOperations.Callback callback) { public void copyFromClipData( final DocumentInfo destination, DocumentStack docStack, final @Nullable ClipData clipData, final FileOperations.Callback callback) { if (clipData == null) { Log.i(TAG, "Received null clipData. Ignoring."); return; Loading Loading @@ -308,7 +370,7 @@ public final class DocumentClipper { * * @return true if the list of files can be copied to destination. */ private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) { private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) { if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) { return false; } Loading src/com/android/documentsui/DocumentsApplication.java +9 −2 Original line number Diff line number Diff line Loading @@ -28,13 +28,14 @@ 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 DocumentClipper mClipper; public static RootsCache getRootsCache(Context context) { Loading Loading @@ -73,7 +74,7 @@ public class DocumentsApplication extends Application { mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4); mClipper = new DocumentClipper(this); mClipper = createClipper(this.getApplicationContext()); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); Loading @@ -88,6 +89,12 @@ 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 src/com/android/documentsui/Files.java 0 → 100644 +38 −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 java.io.File; /** * Utility class for working with {@link File} instances. */ public final class Files { private Files() {} // no initialization for utility classes. public static void deleteRecursively(File file) { if (file.exists()) { if (file.isDirectory()) { for (File child : file.listFiles()) { deleteRecursively(child); } } file.delete(); } } } src/com/android/documentsui/FilesActivity.java +2 −2 Original line number Diff line number Diff line Loading @@ -487,7 +487,7 @@ public class FilesActivity extends BaseActivity { } @Override protected Void run(Uri... params) { public Void run(Uri... params) { final Uri uri = params[0]; final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner); Loading @@ -512,7 +512,7 @@ public class FilesActivity extends BaseActivity { } @Override protected void finish(Void result) { public void finish(Void result) { mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); } } Loading Loading
src/com/android/documentsui/ClipStorage.java 0 → 100644 +136 −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 android.net.Uri; import android.support.annotation.VisibleForTesting; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Provides support for storing lists of documents identified by Uri. * * <li>Access to this object *must* be synchronized externally. * <li>All calls to this class are I/O intensive and must be wrapped in an AsyncTask. */ public final class ClipStorage { private static final String PRIMARY_SELECTION = "primary-selection.txt"; private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes(); private static final int NO_SELECTION_TAG = -1; private final File mOutDir; /** * @param outDir see {@link #prepareStorage(File)}. */ public ClipStorage(File outDir) { assert(outDir.isDirectory()); mOutDir = outDir; } /** * Returns a writer. Callers must... * * <li>synchronize on the {@link ClipStorage} instance while writing to this writer. * <li>closed the write when finished. */ public Writer createWriter() throws IOException { File primary = new File(mOutDir, PRIMARY_SELECTION); return new Writer(new FileOutputStream(primary)); } /** * Saves primary uri list to persistent storage. * @return tag identifying the saved set. */ @VisibleForTesting public long savePrimary() throws IOException { File primary = new File(mOutDir, PRIMARY_SELECTION); if (!primary.exists()) { return NO_SELECTION_TAG; } long tag = System.currentTimeMillis(); File dest = toTagFile(tag); primary.renameTo(dest); return tag; } @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)); } } return uris; } @VisibleForTesting public void delete(long tag) throws IOException { toTagFile(tag).delete(); } private File toTagFile(long tag) { return new File(mOutDir, String.valueOf(tag)); } public static final class Writer implements Closeable { private final FileOutputStream mOut; public Writer(FileOutputStream out) { mOut = out; } public void write(Uri uri) throws IOException { mOut.write(uri.toString().getBytes()); mOut.write(LINE_SEPARATOR); } @Override public void close() throws IOException { mOut.close(); } } /** * Provides initialization and cleanup of the clip data storage directory. */ static File prepareStorage(File cacheDir) { File clipDir = new File(cacheDir, "clippings"); if (clipDir.exists()) { Files.deleteRecursively(clipDir); } assert(!clipDir.exists()); clipDir.mkdir(); return clipDir; } }
src/com/android/documentsui/DocumentClipper.java +119 −57 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.documentsui; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; Loading @@ -27,6 +28,8 @@ import android.provider.DocumentsContract; import android.support.annotation.Nullable; import android.util.Log; import com.android.documentsui.ClipStorage.Writer; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.model.RootInfo; Loading @@ -34,11 +37,13 @@ import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; import com.android.documentsui.services.FileOperations; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; /** * ClipboardManager wrapper class providing higher level logical Loading @@ -49,12 +54,15 @@ public final class DocumentClipper { private static final String TAG = "DocumentClipper"; private static final String SRC_PARENT_KEY = "srcParent"; private static final String OP_TYPE_KEY = "opType"; private static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size"; private Context mContext; private ClipboardManager mClipboard; private final Context mContext; private final ClipStorage mClipStorage; private final ClipboardManager mClipboard; DocumentClipper(Context context) { DocumentClipper(Context context, ClipStorage storage) { mContext = context; mClipStorage = storage; mClipboard = context.getSystemService(ClipboardManager.class); } Loading @@ -79,13 +87,6 @@ public final class DocumentClipper { return uri != null && DocumentsContract.isDocumentUri(mContext, uri); } /** * Returns details regarding the documents on the primary clipboard */ public ClipDetails getClipDetails() { return getClipDetails(mClipboard.getPrimaryClip()); } public ClipDetails getClipDetails(@Nullable ClipData clipData) { if (clipData == null) { return null; Loading Loading @@ -127,54 +128,108 @@ public final class DocumentClipper { } /** * Returns ClipData representing the list of docs, or null if docs is empty, * or docs cannot be converted. * Returns {@link ClipData} representing the selection, or null if selection is empty, * or cannot be converted. */ public @Nullable ClipData getClipDataForDocuments( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(selection != null); if (selection.isEmpty()) { Log.w(TAG, "Attempting to clip empty selection. Ignoring."); return null; } return (selection.size() > Shared.MAX_DOCS_IN_INTENT) ? createJumboClipData(uriBuilder, selection, opType) : createStandardClipData(uriBuilder, selection, opType); } /** * Returns ClipData representing the selection. */ public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs, @OpType int opType) { private @Nullable ClipData createStandardClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(!selection.isEmpty()); final ContentResolver resolver = mContext.getContentResolver(); final String[] mimeTypes = getMimeTypes(resolver, docs); ClipData clipData = null; for (DocumentInfo doc : docs) { assert(doc != null); assert(doc.derivedUri != null); if (clipData == null) { // TODO: figure out what this string should be. // Currently it is not displayed anywhere in the UI, but this might change. final String clipLabel = ""; clipData = new ClipData(clipLabel, mimeTypes, new ClipData.Item(doc.derivedUri)); final ArrayList<ClipData.Item> clipItems = new ArrayList<>(); final Set<String> clipTypes = new HashSet<>(); PersistableBundle bundle = new PersistableBundle(); bundle.putInt(OP_TYPE_KEY, opType); clipData.getDescription().setExtras(bundle); } else { // TODO: update list of mime types in ClipData. clipData.addItem(new ClipData.Item(doc.derivedUri)); } int clipCount = 0; for (String id : selection) { assert(id != null); Uri uri = uriBuilder.apply(id); if (clipCount <= Shared.MAX_DOCS_IN_INTENT) { DocumentInfo.addMimeTypes(resolver, uri, clipTypes); clipItems.add(new ClipData.Item(uri)); } return clipData; clipCount++; } private static String[] getMimeTypes(ContentResolver resolver, List<DocumentInfo> docs) { final HashSet<String> mimeTypes = new HashSet<>(); for (DocumentInfo doc : docs) { assert(doc != null); assert(doc.derivedUri != null); final Uri uri = doc.derivedUri; if ("content".equals(uri.getScheme())) { mimeTypes.add(resolver.getType(uri)); final String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); if (streamTypes != null) { mimeTypes.addAll(Arrays.asList(streamTypes)); ClipDescription description = new ClipDescription( "", // Currently "label" is not displayed anywhere in the UI. clipTypes.toArray(new String[0])); description.setExtras(bundle); return new ClipData(description, clipItems); } /** * Returns ClipData representing the list of docs, or null if docs is empty, * or docs cannot be converted. */ private @Nullable ClipData createJumboClipData( Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) { assert(!selection.isEmpty()); final ContentResolver resolver = mContext.getContentResolver(); final ArrayList<ClipData.Item> clipItems = new ArrayList<>(); final Set<String> clipTypes = new HashSet<>(); PersistableBundle bundle = new PersistableBundle(); bundle.putInt(OP_TYPE_KEY, opType); bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size()); int clipCount = 0; synchronized (mClipStorage) { try (Writer writer = mClipStorage.createWriter()) { for (String id : selection) { assert(id != null); Uri uri = uriBuilder.apply(id); if (clipCount <= Shared.MAX_DOCS_IN_INTENT) { DocumentInfo.addMimeTypes(resolver, uri, clipTypes); clipItems.add(new ClipData.Item(uri)); } writer.write(uri); clipCount++; } } catch (IOException e) { Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e); return null; } } return mimeTypes.toArray(new String[0]); ClipDescription description = new ClipDescription( "", // Currently "label" is not displayed anywhere in the UI. clipTypes.toArray(new String[0])); description.setExtras(bundle); return new ClipData(description, clipItems); } /** * Puts {@code ClipData} in a primary clipboard, describing a copy operation */ public void clipDocumentsForCopy(List<DocumentInfo> docs) { ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_COPY); public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) { ClipData data = getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY); assert(data != null); mClipboard.setPrimaryClip(data); Loading @@ -183,24 +238,24 @@ public final class DocumentClipper { /** * Puts {@Code ClipData} in a primary clipboard, describing a cut operation */ public void clipDocumentsForCut(List<DocumentInfo> docs, DocumentInfo srcParent) { assert(docs != null); assert(!docs.isEmpty()); assert(srcParent != null); assert(srcParent.derivedUri != null); public void clipDocumentsForCut( Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) { assert(!selection.isEmpty()); assert(parent.derivedUri != null); ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_MOVE); ClipData data = getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_MOVE); assert(data != null); PersistableBundle bundle = data.getDescription().getExtras(); bundle.putString(SRC_PARENT_KEY, srcParent.derivedUri.toString()); bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString()); mClipboard.setPrimaryClip(data); } private DocumentInfo createDocument(Uri uri) { DocumentInfo doc = null; if (uri != null && DocumentsContract.isDocumentUri(mContext, uri)) { if (isDocumentUri(uri)) { ContentResolver resolver = mContext.getContentResolver(); try { doc = DocumentInfo.fromUri(resolver, uri); Loading @@ -219,8 +274,11 @@ public final class DocumentClipper { * @param docStack the document stack to the destination folder, * @param callback callback to notify when operation finishes. */ public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack, public void copyFromClipboard( DocumentInfo destination, DocumentStack docStack, FileOperations.Callback callback) { copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback); } Loading @@ -232,8 +290,12 @@ public final class DocumentClipper { * @param clipData the clipData to copy from, or null to copy from clipboard * @param callback callback to notify when operation finishes */ public void copyFromClipData(final DocumentInfo destination, DocumentStack docStack, @Nullable final ClipData clipData, final FileOperations.Callback callback) { public void copyFromClipData( final DocumentInfo destination, DocumentStack docStack, final @Nullable ClipData clipData, final FileOperations.Callback callback) { if (clipData == null) { Log.i(TAG, "Received null clipData. Ignoring."); return; Loading Loading @@ -308,7 +370,7 @@ public final class DocumentClipper { * * @return true if the list of files can be copied to destination. */ private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) { private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) { if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) { return false; } Loading
src/com/android/documentsui/DocumentsApplication.java +9 −2 Original line number Diff line number Diff line Loading @@ -28,13 +28,14 @@ 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 DocumentClipper mClipper; public static RootsCache getRootsCache(Context context) { Loading Loading @@ -73,7 +74,7 @@ public class DocumentsApplication extends Application { mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4); mClipper = new DocumentClipper(this); mClipper = createClipper(this.getApplicationContext()); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); Loading @@ -88,6 +89,12 @@ 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
src/com/android/documentsui/Files.java 0 → 100644 +38 −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 java.io.File; /** * Utility class for working with {@link File} instances. */ public final class Files { private Files() {} // no initialization for utility classes. public static void deleteRecursively(File file) { if (file.exists()) { if (file.isDirectory()) { for (File child : file.listFiles()) { deleteRecursively(child); } } file.delete(); } } }
src/com/android/documentsui/FilesActivity.java +2 −2 Original line number Diff line number Diff line Loading @@ -487,7 +487,7 @@ public class FilesActivity extends BaseActivity { } @Override protected Void run(Uri... params) { public Void run(Uri... params) { final Uri uri = params[0]; final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner); Loading @@ -512,7 +512,7 @@ public class FilesActivity extends BaseActivity { } @Override protected void finish(Void result) { public void finish(Void result) { mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); } } Loading