[multi-part] Eliminate 1k selection limit

* Remove clearing clip files at app launch.
* Remove clip files when they won't be used.
* Stop adding clip items to jumbo clip data.
* Repurpose ClipDetails to hold operation type, srcParent and file list.
* Use ClipDetails for copyTo/moveTo/delete menu actions.
* Make FileOperationService consume ClipDetails.
* Add set up state to jobs; show set up notification in set up phase.
* Make copy job cancelable at set up phase.

Bug: 28194201
Change-Id: I2018ff9bc51515c1f6a029f0ede3f4c7d2beee43
(cherry picked from commit 664881e4b1fdc3416648f000287f52168978364f)
This commit is contained in:
Garfield, Tan
2016-06-17 15:32:28 -07:00
committed by Garfield Tan
parent 42e62608e5
commit f46958bebd
27 changed files with 1211 additions and 648 deletions

View File

@@ -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>

View File

@@ -0,0 +1,343 @@
/*
* 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];
}
};
}
}

View File

@@ -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,46 +53,27 @@ 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.
*/
@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;
public Writer createWriter(long tag) throws IOException {
File file = toTagFile(tag);
return new Writer(file);
}
@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;
public Reader createReader(long tag) throws IOException {
File file = toTagFile(tag);
return new Reader(file);
}
@VisibleForTesting
@@ -102,12 +85,87 @@ public final class ClipStorage {
return new File(mOutDir, String.valueOf(tag));
}
public static final class Writer implements Closeable {
/**
* Provides initialization of the clip data storage directory.
*/
static File prepareStorage(File cacheDir) {
File clipDir = getClipDir(cacheDir);
clipDir.mkdir();
assert(clipDir.isDirectory());
return clipDir;
}
public static boolean hasDocList(long tag) {
return tag != NO_SELECTION_TAG;
}
private static File getClipDir(File cacheDir) {
return new File(cacheDir, "clippings");
}
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);
}
@Override
public Iterator iterator() {
return new Iterator(mScanner);
}
@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);
}
}
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 {
mOut.close();
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;
}
}

View File

@@ -21,25 +21,23 @@ import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.BaseBundle;
import android.os.PersistableBundle;
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;
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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -49,26 +47,49 @@ import java.util.function.Function;
* ClipboardManager wrapper class providing higher level logical
* support for dealing with Documents.
*/
public final class DocumentClipper {
public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChangedListener {
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";
static final String SRC_PARENT_KEY = "srcParent";
static final String OP_TYPE_KEY = "opType";
static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
static final String OP_JUMBO_SELECTION_TAG = "jumboSelection-tag";
// Use shared preference to store last seen primary clip tag, so that we can delete the file
// when we realize primary clip has been changed when we're not running.
private static final String PREF_NAME = "DocumentClipperPref";
private static final String LAST_PRIMARY_CLIP_TAG = "lastPrimaryClipTag";
private final Context mContext;
private final ClipStorage mClipStorage;
private final ClipboardManager mClipboard;
// Here we're tracking the last clipped tag ids so we can delete them later.
private long mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
private long mLastUnusedPrimaryClipTag = ClipStorage.NO_SELECTION_TAG;
private final SharedPreferences mPref;
DocumentClipper(Context context, ClipStorage storage) {
mContext = context;
mClipStorage = storage;
mClipboard = context.getSystemService(ClipboardManager.class);
mClipboard.addPrimaryClipChangedListener(this);
// Primary clips may be changed when we're not running, now it's time to clean up the
// remnant.
mPref = context.getSharedPreferences(PREF_NAME, 0);
mLastUnusedPrimaryClipTag =
mPref.getLong(LAST_PRIMARY_CLIP_TAG, ClipStorage.NO_SELECTION_TAG);
deleteLastUnusedPrimaryClip();
}
public boolean hasItemsToPaste() {
if (mClipboard.hasPrimaryClip()) {
ClipData clipData = mClipboard.getPrimaryClip();
int count = clipData.getItemCount();
if (count > 0) {
for (int i = 0; i < count; ++i) {
@@ -87,51 +108,27 @@ public final class DocumentClipper {
return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
}
public ClipDetails getClipDetails(@Nullable ClipData clipData) {
if (clipData == null) {
return null;
}
/**
* Returns {@link ClipData} representing the selection, or null if selection is empty,
* or cannot be converted.
*
* This is specialized for drag and drop so that we know which file to delete if nobody accepts
* the drop.
*/
public @Nullable ClipData getClipDataForDrag(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
ClipData data = getClipDataForDocuments(uriBuilder, selection, opType);
String srcParent = clipData.getDescription().getExtras().getString(SRC_PARENT_KEY);
mLastDragClipTag = getTag(data);
ClipDetails clipDetails = new ClipDetails(
clipData.getDescription().getExtras().getInt(OP_TYPE_KEY),
getDocumentsFromClipData(clipData),
createDocument((srcParent != null) ? Uri.parse(srcParent) : null));
return clipDetails;
}
private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
assert(clipData != null);
int count = clipData.getItemCount();
if (count == 0) {
return Collections.EMPTY_LIST;
}
final List<DocumentInfo> srcDocs = new ArrayList<>();
for (int i = 0; i < count; ++i) {
ClipData.Item item = clipData.getItemAt(i);
Uri itemUri = item.getUri();
DocumentInfo docInfo = createDocument(itemUri);
if (docInfo != null) {
srcDocs.add(docInfo);
} else {
// This uri either doesn't exist, or is invalid.
Log.w(TAG, "Can't create document info from uri: " + itemUri);
}
}
return srcDocs;
return data;
}
/**
* Returns {@link ClipData} representing the selection, or null if selection is empty,
* or cannot be converted.
*/
public @Nullable ClipData getClipDataForDocuments(
private @Nullable ClipData getClipDataForDocuments(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(selection != null);
@@ -153,6 +150,7 @@ public final class DocumentClipper {
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(!selection.isEmpty());
assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
final ContentResolver resolver = mContext.getContentResolver();
final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
@@ -161,15 +159,11 @@ public final class DocumentClipper {
PersistableBundle bundle = new PersistableBundle();
bundle.putInt(OP_TYPE_KEY, opType);
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));
}
clipCount++;
DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
clipItems.add(new ClipData.Item(uri));
}
ClipDescription description = new ClipDescription(
@@ -181,46 +175,51 @@ public final class DocumentClipper {
}
/**
* Returns ClipData representing the list of docs, or null if docs is empty,
* or docs cannot be converted.
* Returns ClipData representing the list of docs
*/
private @Nullable ClipData createJumboClipData(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(!selection.isEmpty());
assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
final List<Uri> uris = new ArrayList<>(selection.size());
final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
// Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
final ContentResolver resolver = mContext.getContentResolver();
final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
final Set<String> clipTypes = new HashSet<>();
int docCount = 0;
for (String id : selection) {
assert(id != null);
Uri uri = uriBuilder.apply(id);
if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
clipItems.add(new ClipData.Item(uri));
}
uris.add(uri);
}
// Prepare metadata
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;
}
}
// Creates a clip tag
long tag = mClipStorage.createTag();
bundle.putLong(OP_JUMBO_SELECTION_TAG, tag);
ClipDescription description = new ClipDescription(
"", // Currently "label" is not displayed anywhere in the UI.
clipTypes.toArray(new String[0]));
description.setExtras(bundle);
// Persists clip items
new ClipStorage.PersistTask(mClipStorage, uris, tag).execute();
return new ClipData(description, clipItems);
}
@@ -232,7 +231,7 @@ public final class DocumentClipper {
getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
assert(data != null);
mClipboard.setPrimaryClip(data);
setPrimaryClip(data);
}
/**
@@ -250,20 +249,65 @@ public final class DocumentClipper {
PersistableBundle bundle = data.getDescription().getExtras();
bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
setPrimaryClip(data);
}
private void setPrimaryClip(ClipData data) {
deleteLastPrimaryClip();
long tag = getTag(data);
setLastUnusedPrimaryClipTag(tag);
mClipboard.setPrimaryClip(data);
}
private DocumentInfo createDocument(Uri uri) {
DocumentInfo doc = null;
if (isDocumentUri(uri)) {
ContentResolver resolver = mContext.getContentResolver();
try {
doc = DocumentInfo.fromUri(resolver, uri);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
/**
* Sets this primary tag to both class variable and shared preference.
*/
private void setLastUnusedPrimaryClipTag(long tag) {
mLastUnusedPrimaryClipTag = tag;
mPref.edit().putLong(LAST_PRIMARY_CLIP_TAG, tag).commit();
}
/**
* This is a good chance for us to remove previous clip file for cut/copy because we know a new
* primary clip is set.
*/
@Override
public void onPrimaryClipChanged() {
deleteLastUnusedPrimaryClip();
}
private void deleteLastUnusedPrimaryClip() {
ClipData primary = mClipboard.getPrimaryClip();
long primaryTag = getTag(primary);
// onPrimaryClipChanged is also called after we call setPrimaryClip(), so make sure we don't
// delete the clip file we just created.
if (mLastUnusedPrimaryClipTag != primaryTag) {
deleteLastPrimaryClip();
}
}
private void deleteLastPrimaryClip() {
deleteClip(mLastUnusedPrimaryClipTag);
setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
}
/**
* Deletes the last seen drag clip file.
*/
public void deleteDragClip() {
deleteClip(mLastDragClipTag);
mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
}
private void deleteClip(long tag) {
try {
mClipStorage.delete(tag);
} catch (IOException e) {
Log.w(TAG, "Error deleting clip file with tag: " + tag, e);
}
return doc;
}
/**
@@ -279,6 +323,10 @@ public final class DocumentClipper {
DocumentStack docStack,
FileOperations.Callback callback) {
// The primary clip has been claimed by a file operation. It's now the operation's duty
// to make sure the clip file is deleted after use.
setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
}
@@ -301,65 +349,28 @@ public final class DocumentClipper {
return;
}
new AsyncTask<Void, Void, ClipDetails>() {
ClipDetails details = ClipDetails.createClipDetails(clipData);
@Override
protected ClipDetails doInBackground(Void... params) {
return getClipDetails(clipData);
}
@Override
protected void onPostExecute(ClipDetails clipDetails) {
if (clipDetails == null) {
Log.w(TAG, "Received null clipDetails. Ignoring.");
return;
}
List<DocumentInfo> docs = clipDetails.docs;
@OpType int type = clipDetails.opType;
DocumentInfo srcParent = clipDetails.parent;
moveDocuments(docs, destination, docStack, type, srcParent, callback);
}
}.execute();
}
/**
* Moves {@code docs} from {@code srcParent} to {@code destination}.
* operationType can be copy or cut
* srcParent Must be non-null for move operations.
*/
private void moveDocuments(
List<DocumentInfo> docs,
DocumentInfo destination,
DocumentStack docStack,
@OpType int operationType,
DocumentInfo srcParent,
FileOperations.Callback callback) {
RootInfo destRoot = docStack.root;
if (!canCopy(docs, destRoot, destination)) {
callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, operationType, 0);
if (!canCopy(destination)) {
callback.onOperationResult(
FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0);
return;
}
if (docs.isEmpty()) {
callback.onOperationResult(FileOperations.Callback.STATUS_ACCEPTED, operationType, 0);
if (details.getItemCount() == 0) {
callback.onOperationResult(
FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 0);
return;
}
DocumentStack dstStack = new DocumentStack();
dstStack.push(destination);
dstStack.addAll(docStack);
switch (operationType) {
case FileOperationService.OPERATION_MOVE:
FileOperations.move(mContext, docs, srcParent, dstStack, callback);
break;
case FileOperationService.OPERATION_COPY:
FileOperations.copy(mContext, docs, dstStack, callback);
break;
default:
throw new UnsupportedOperationException("Unsupported operation: " + operationType);
}
// Pass root here so that we can perform "download" root check when
dstStack.root = docStack.root;
FileOperations.start(mContext, details, dstStack, callback);
}
/**
@@ -370,32 +381,26 @@ public final class DocumentClipper {
*
* @return true if the list of files can be copied to destination.
*/
private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
private static boolean canCopy(DocumentInfo dest) {
if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
return false;
}
// Can't copy folders to downloads, because we don't show folders there.
if (root.isDownloads()) {
for (DocumentInfo docs : files) {
if (docs.isDirectory()) {
return false;
}
}
}
return true;
}
public static class ClipDetails {
public final @OpType int opType;
public final List<DocumentInfo> docs;
public final @Nullable DocumentInfo parent;
ClipDetails(@OpType int opType, List<DocumentInfo> docs, @Nullable DocumentInfo parent) {
this.opType = opType;
this.docs = docs;
this.parent = parent;
/**
* Obtains tag from {@link ClipData}. Returns {@link ClipStorage#NO_SELECTION_TAG}
* if it's not a jumbo clip.
*/
private static long getTag(@Nullable ClipData data) {
if (data == null) {
return ClipStorage.NO_SELECTION_TAG;
}
ClipDescription description = data.getDescription();
BaseBundle bundle = description.getExtras();
return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
}
}

View File

@@ -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);

View File

@@ -24,6 +24,12 @@ import android.view.View;
public final class Snackbars {
private Snackbars() {}
public static final void showDocumentsClipped(Activity activity, int docCount) {
String msg = Shared.getQuantityString(
activity, R.plurals.clipboard_files_clipped, docCount);
Snackbars.makeSnackbar(activity, msg, Snackbar.LENGTH_SHORT).show();
}
public static final void showMove(Activity activity, int docCount) {
CharSequence message = Shared.getQuantityString(activity, R.plurals.move_begin, docCount);
makeSnackbar(activity, message, Snackbar.LENGTH_SHORT).show();

View File

@@ -25,7 +25,6 @@ import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
@@ -123,9 +122,6 @@ public class State implements android.os.Parcelable {
/** Instance state for every shown directory */
public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>();
/** Currently copying file */
public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<>();
/** Name of the package that started DocsUI */
public List<String> excludedAuthorities = new ArrayList<>();
@@ -199,7 +195,6 @@ public class State implements android.os.Parcelable {
out.writeInt(external ? 1 : 0);
DurableUtils.writeToParcel(out, stack);
out.writeMap(dirState);
out.writeList(selectedDocumentsForCopy);
out.writeList(excludedAuthorities);
out.writeInt(openableOnly ? 1 : 0);
out.writeInt(mStackTouched ? 1 : 0);
@@ -229,7 +224,6 @@ public class State implements android.os.Parcelable {
state.external = in.readInt() != 0;
DurableUtils.readFromParcel(in, state.stack);
in.readMap(state.dirState, loader);
in.readList(state.selectedDocumentsForCopy, loader);
in.readList(state.excludedAuthorities, loader);
state.openableOnly = in.readInt() != 0;
state.mStackTouched = in.readInt() != 0;

View File

@@ -1,61 +0,0 @@
/*
* 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.dirlist;
import android.app.Activity;
import android.os.AsyncTask;
import android.support.design.widget.Snackbar;
import com.android.documentsui.R;
import com.android.documentsui.Shared;
import com.android.documentsui.Snackbars;
/**
* AsyncTask that performs a supplied runnable (presumably doing some clippy thing)in background,
* then shows a toast reciting how many fantastic things have been clipped.
*/
final class ClipTask extends AsyncTask<Void, Void, Void> {
private Runnable mOperation;
private int mSelectionSize;
private Activity mActivity;
ClipTask(Activity activity, Runnable operation, int selectionSize) {
mActivity = activity;
mOperation = operation;
mSelectionSize = selectionSize;
}
@Override
protected Void doInBackground(Void... params) {
// Clip operation varies (cut or past) and has different inputs.
// To increase sharing we accept the no ins/outs operation as a plain runnable.
mOperation.run();
return null;
}
@Override
protected void onPostExecute(Void result) {
String msg = Shared.getQuantityString(
mActivity,
R.plurals.clipboard_files_clipped,
mSelectionSize);
Snackbars.makeSnackbar(mActivity, msg, Snackbar.LENGTH_SHORT)
.show();
}
}

View File

@@ -31,8 +31,16 @@ class DirectoryDragListener extends ItemDragListener<DirectoryFragment> {
public boolean onDrag(View v, DragEvent event) {
final boolean result = super.onDrag(v, event);
if (event.getAction() == DragEvent.ACTION_DRAG_ENDED && event.getResult()) {
mDragHost.clearSelection();
if (event.getAction() == DragEvent.ACTION_DRAG_ENDED) {
// getResult() is true if drag was accepted
if (event.getResult()) {
mDragHost.clearSelection();
} else {
// When drag starts we might write a new clip file to disk.
// No drop event happens, remove clip file here. This may be called multiple times,
// but it should be OK because deletion is idempotent and cheap.
mDragHost.deleteDragClipFile();
}
}
return result;

View File

@@ -72,10 +72,10 @@ import android.widget.TextView;
import android.widget.Toolbar;
import com.android.documentsui.BaseActivity;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentClipper;
import com.android.documentsui.DocumentClipper.ClipDetails;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events;
@@ -96,7 +96,6 @@ import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
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;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -171,6 +170,9 @@ public class DirectoryFragment extends Fragment
// Note, we use !null to indicate that selection was restored (from rotation).
// So don't fiddle with this field unless you've got the bigger picture in mind.
private @Nullable Selection mRestoredSelection = null;
// Here we save the clip details of moveTo/copyTo actions when picker shows up.
// This will be written to saved instance.
private @Nullable ClipDetails mDetailsForCopy;
private boolean mSearchMode = false;
private @Nullable BandController mBandController;
@@ -200,6 +202,9 @@ public class DirectoryFragment extends Fragment
case FileOperationService.OPERATION_COPY:
Snackbars.showCopy(getActivity(), docCount);
break;
case FileOperationService.OPERATION_DELETE:
// We don't show anything for deletion.
break;
default:
throw new UnsupportedOperationException("Unsupported Operation: " + opType);
}
@@ -264,6 +269,7 @@ public class DirectoryFragment extends Fragment
mQuery = args.getString(Shared.EXTRA_QUERY);
mType = args.getInt(Shared.EXTRA_TYPE);
mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
mDetailsForCopy = args.getParcelable(FileOperationService.EXTRA_CLIP_DETAILS);
// Restore any selection we may have squirreled away in retained state.
@Nullable RetainedState retained = getBaseActivity().getRetainedState();
@@ -353,6 +359,7 @@ public class DirectoryFragment extends Fragment
outState.putParcelable(Shared.EXTRA_DOC, mDocument);
outState.putString(Shared.EXTRA_QUERY, mQuery);
outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
outState.putParcelable(FileOperationService.EXTRA_CLIP_DETAILS, mDetailsForCopy);
}
@Override
@@ -392,22 +399,21 @@ public class DirectoryFragment extends Fragment
}
private void handleCopyResult(int resultCode, Intent data) {
ClipDetails details = mDetailsForCopy;
mDetailsForCopy = null;
if (resultCode == Activity.RESULT_CANCELED || data == null) {
// User pressed the back button or otherwise cancelled the destination pick. Don't
// proceed with the copy.
details.dispose(getContext());
return;
}
@OpType int operationType = data.getIntExtra(
FileOperationService.EXTRA_OPERATION,
FileOperationService.OPERATION_COPY);
FileOperations.start(
getContext(),
getDisplayState().selectedDocumentsForCopy,
getDisplayState().stack.peek(),
(DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
operationType,
details,
data.getParcelableExtra(Shared.EXTRA_STACK),
mFileOpCallback);
}
@@ -984,8 +990,14 @@ public class DirectoryFragment extends Fragment
Log.w(TAG, "Action mode is null before deleting documents.");
}
FileOperations.delete(
getActivity(), docs, srcParent, getDisplayState().stack);
ClipDetails details = ClipDetails.createClipDetails(
FileOperationService.OPERATION_DELETE,
srcParent.derivedUri,
selected,
mModel::getItemUri,
getContext());
FileOperations.start(getActivity(), details,
getDisplayState().stack, mFileOpCallback);
}
})
.setNegativeButton(android.R.string.cancel, null)
@@ -1009,6 +1021,9 @@ public class DirectoryFragment extends Fragment
getActivity(),
DocumentsActivity.class);
Uri srcParent = getDisplayState().stack.peek().derivedUri;
mDetailsForCopy = ClipDetails.createClipDetails(
mode, srcParent, selected, mModel::getItemUri, getContext());
// Relay any config overrides bits present in the original intent.
Intent original = getActivity().getIntent();
@@ -1028,15 +1043,11 @@ public class DirectoryFragment extends Fragment
new GetDocumentsTask() {
@Override
void onDocumentsReady(List<DocumentInfo> docs) {
// TODO: Can this move to Fragment bundle state?
getDisplayState().selectedDocumentsForCopy = docs;
// Determine if there is a directory in the set of documents
// to be copied? Why? Directory creation isn't supported by some roots
// (like Downloads). This informs DocumentsActivity (the "picker")
// to restrict available roots to just those with support.
intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
// This just identifies the type of request...we'll check it
// when we reveive a response.
@@ -1164,13 +1175,9 @@ public class DirectoryFragment extends Fragment
}
mSelectionManager.clearSelection();
// Clips the docs in the background, then displays a message
new ClipTask(
getActivity(),
() -> {
mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
},
selection.size()).execute();
mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
Snackbars.showDocumentsClipped(getActivity(), selection.size());
}
public void cutSelectedToClipboard() {
@@ -1182,16 +1189,9 @@ public class DirectoryFragment extends Fragment
}
mSelectionManager.clearSelection();
// Clips the docs in the background, then displays a message
new ClipTask(
getActivity(),
() -> {
mClipper.clipDocumentsForCut(
mModel::getItemUri,
selection,
getDisplayState().stack.peek());
},
selection.size()).execute();
mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
Snackbars.showDocumentsClipped(getActivity(), selection.size());
}
public void pasteFromClipboard() {
@@ -1273,15 +1273,19 @@ public class DirectoryFragment extends Fragment
activity.setRootsDrawerOpen(false);
}
public boolean handleDropEvent(View v, DragEvent event) {
void deleteDragClipFile() {
mClipper.deleteDragClip();
}
boolean handleDropEvent(View v, DragEvent event) {
BaseActivity activity = (BaseActivity) getActivity();
activity.setRootsDrawerOpen(false);
ClipData clipData = event.getClipData();
assert (clipData != null);
ClipDetails clipDetails = mClipper.getClipDetails(clipData);
assert(clipDetails.opType == FileOperationService.OPERATION_COPY);
assert(ClipDetails.createClipDetails(clipData).getOpType()
== FileOperationService.OPERATION_COPY);
// Don't copy from the cwd into the cwd. Note: this currently doesn't work for
// multi-window drag, because localState isn't carried over from one process to
@@ -1478,7 +1482,7 @@ public class DirectoryFragment extends Fragment
// the current code layout and framework assumptions don't support
// this. So for now, we could end up doing a bunch of i/o on main thread.
v.startDragAndDrop(
mClipper.getClipDataForDocuments(
mClipper.getClipDataForDrag(
mModel::getItemUri,
selection,
FileOperationService.OPERATION_COPY),

View File

@@ -29,13 +29,13 @@ import static com.android.documentsui.model.DocumentInfo.getCursorString;
import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import android.annotation.StringRes;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
@@ -50,11 +50,12 @@ import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.model.RootInfo;
import libcore.io.IoUtils;
@@ -83,30 +84,18 @@ class CopyJob extends Job {
private long mRemainingTime;
/**
* Copies files to a destination identified by {@code destination}.
* @see @link {@link Job} constructor for most param descriptions.
*
* @param srcs List of files to be copied.
* @param details clip details containing source file list
*/
CopyJob(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs) {
super(service, appContext, listener, OPERATION_COPY, id, stack);
String id, DocumentStack destination, ClipDetails details) {
super(service, appContext, listener, id, destination, details);
assert(!srcs.isEmpty());
this.mSrcs = srcs;
}
assert(details.getItemCount() > 0);
/**
* @see @link {@link Job} constructor for most param descriptions.
*
* @param srcs List of files to be copied.
*/
CopyJob(Context service, Context appContext, Listener listener,
@OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
super(service, appContext, listener, opType, id, destination);
assert(!srcs.isEmpty());
this.mSrcs = srcs;
// delay the initialization of it to setUp() because it may be IO extensive.
mSrcs = new ArrayList<>(details.getItemCount());
}
@Override
@@ -167,7 +156,7 @@ class CopyJob extends Job {
// mBytesCopied is modified in worker thread, but this method is called in monitor thread,
// so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
final long bytesCopied = mBytesCopied;
final long sampleDuration = elapsedTime - mSampleTime;
final long sampleDuration = Math.max(elapsedTime - mSampleTime, 1L); // avoid dividing 0
final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
if (mSpeed == 0) {
mSpeed = sampleSpeed;
@@ -215,8 +204,18 @@ class CopyJob extends Job {
}
@Override
void start() {
mStartTime = elapsedRealtime();
boolean setUp() {
try {
buildDocumentList();
} catch (ResourceException e) {
Log.e(TAG, "Failed to get the list of docs.", e);
return false;
}
if (isCanceled()) {
return false;
}
try {
mBatchSize = calculateSize(mSrcs);
@@ -225,6 +224,12 @@ class CopyJob extends Job {
mBatchSize = -1;
}
return true;
}
@Override
void start() {
mStartTime = elapsedRealtime();
DocumentInfo srcInfo;
DocumentInfo dstInfo = stack.peek();
for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
@@ -249,6 +254,33 @@ class CopyJob extends Job {
Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
}
private void buildDocumentList() throws ResourceException {
try {
final ContentResolver resolver = appContext.getContentResolver();
final Iterable<Uri> uris = details.getDocs(appContext);
for (Uri uri : uris) {
DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
if (canCopy(doc, stack.root)) {
mSrcs.add(doc);
} else {
onFileFailed(doc);
}
if (isCanceled()) {
return;
}
}
} catch(IOException e) {
failedFileCount += details.getItemCount();
throw new ResourceException("Failed to open the list of docs to copy.", e);
}
}
private static boolean canCopy(DocumentInfo doc, RootInfo root) {
// Can't copy folders to downloads, because we don't show folders there.
return !root.isDownloads() || !doc.isDirectory();
}
@Override
boolean hasWarnings() {
return !convertedFiles.isEmpty();
@@ -553,6 +585,10 @@ class CopyJob extends Job {
} else {
result += src.size;
}
if (isCanceled()) {
return result;
}
}
return result;
}
@@ -562,7 +598,7 @@ class CopyJob extends Job {
*
* @throws ResourceException
*/
private static long calculateFileSizesRecursively(
private long calculateFileSizesRecursively(
ContentProviderClient client, Uri uri) throws ResourceException {
final String authority = uri.getAuthority();
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
@@ -576,7 +612,7 @@ class CopyJob extends Job {
Cursor cursor = null;
try {
cursor = client.query(queryUri, queryColumns, null, null, null);
while (cursor.moveToNext()) {
while (cursor.moveToNext() && !isCanceled()) {
if (Document.MIME_TYPE_DIR.equals(
getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
// Recurse into directories.
@@ -623,7 +659,7 @@ class CopyJob extends Job {
.append("CopyJob")
.append("{")
.append("id=" + id)
.append(", srcs=" + mSrcs)
.append(", details=" + details)
.append(", destination=" + stack)
.append("}")
.toString();

View File

@@ -17,25 +17,27 @@
package com.android.documentsui.services;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
final class DeleteJob extends Job {
private static final String TAG = "DeleteJob";
private List<DocumentInfo> mSrcs;
final DocumentInfo mSrcParent;
private volatile int mDocsProcessed = 0;
@@ -46,14 +48,11 @@ final class DeleteJob extends Job {
*
* @see @link {@link Job} constructor for most param descriptions.
*
* @param srcs List of files to delete.
* @param srcParent Parent of all source files.
* @param details details that contains files to be deleted and their parent
*/
DeleteJob(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs, DocumentInfo srcParent) {
super(service, appContext, listener, OPERATION_DELETE, id, stack);
this.mSrcs = srcs;
this.mSrcParent = srcParent;
String id, DocumentStack stack, ClipDetails details) {
super(service, appContext, listener, id, stack, details);
}
@Override
@@ -72,9 +71,9 @@ final class DeleteJob extends Job {
@Override
public Notification getProgressNotification() {
mProgressBuilder.setProgress(mSrcs.size(), mDocsProcessed, false);
mProgressBuilder.setProgress(details.getItemCount(), mDocsProcessed, false);
String format = service.getString(R.string.delete_progress);
mProgressBuilder.setSubText(String.format(format, mDocsProcessed, mSrcs.size()));
mProgressBuilder.setSubText(String.format(format, mDocsProcessed, details.getItemCount()));
mProgressBuilder.setContentText(null);
@@ -94,23 +93,37 @@ final class DeleteJob extends Job {
@Override
void start() {
for (DocumentInfo doc : mSrcs) {
if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
try {
deleteDocument(doc, mSrcParent);
try {
final List<DocumentInfo> srcs = new ArrayList<>(details.getItemCount());
if (isCanceled()) {
// Canceled, dump the rest of the work. Deleted docs are not recoverable.
return;
final Iterable<Uri> uris = details.getDocs(appContext);
final ContentResolver resolver = appContext.getContentResolver();
final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
for (Uri uri : uris) {
DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
srcs.add(doc);
if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
try {
deleteDocument(doc, srcParent);
if (isCanceled()) {
// Canceled, dump the rest of the work. Deleted docs are not recoverable.
return;
}
} catch (ResourceException e) {
Log.e(TAG, "Failed to delete document @ " + doc.derivedUri, e);
onFileFailed(doc);
}
} catch (ResourceException e) {
Log.e(TAG, "Failed to delete document @ " + doc.derivedUri, e);
onFileFailed(doc);
}
++mDocsProcessed;
++mDocsProcessed;
}
Metrics.logFileOperation(service, operationType, srcs, null);
} catch(IOException e) {
Log.e(TAG, "Failed to get list of docs or parent source.", e);
failedFileCount += details.getItemCount();
}
Metrics.logFileOperation(service, operationType, mSrcs, null);
}
@Override
@@ -119,8 +132,7 @@ final class DeleteJob extends Job {
.append("DeleteJob")
.append("{")
.append("id=" + id)
.append(", srcs=" + mSrcs)
.append(", srcParent=" + mSrcParent)
.append(", details=" + details)
.append(", location=" + stack)
.append("}")
.toString();

View File

@@ -29,8 +29,8 @@ import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.Shared;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.Job.Factory;
@@ -58,13 +58,10 @@ public class FileOperationService extends Service implements Job.Listener {
public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
public static final String EXTRA_CLIP_DETAILS = "com.android.documentsui.SRC_CLIP_DETAIL";
public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
// This extra 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.
public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
@IntDef(flag = true, value = {
OPERATION_UNKNOWN,
@@ -145,6 +142,7 @@ public class FileOperationService extends Service implements Job.Listener {
executor = null;
deletionExecutor = null;
handler = null;
if (DEBUG) Log.d(TAG, "Destroyed.");
}
@@ -154,35 +152,33 @@ public class FileOperationService extends Service implements Job.Listener {
// checkArgument(flags == 0); // retry and redeliver are not supported.
String jobId = intent.getStringExtra(EXTRA_JOB_ID);
@OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
assert(jobId != null);
if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
if (intent.hasExtra(EXTRA_CANCEL)) {
handleCancel(intent);
} else {
assert(operationType != OPERATION_UNKNOWN);
handleOperation(intent, serviceId, jobId, operationType);
ClipDetails details = intent.getParcelableExtra(EXTRA_CLIP_DETAILS);
assert(details.getOpType() != OPERATION_UNKNOWN);
handleOperation(intent, jobId, details);
}
return START_NOT_STICKY;
}
private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
// Track the service supplied id so we can stop the service once we're out of work to do.
mLastServiceId = serviceId;
return START_NOT_STICKY;
}
private void handleOperation(Intent intent, String jobId, ClipDetails details) {
synchronized (mRunning) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
Job job = createJob(operationType, jobId, srcs, srcParent, stack);
Job job = createJob(jobId, details, stack);
if (job == null) {
return;
@@ -192,7 +188,7 @@ public class FileOperationService extends Service implements Job.Listener {
assert (job != null);
if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
Future<?> future = getExecutorService(operationType).submit(job);
Future<?> future = getExecutorService(details.getOpType()).submit(job);
mRunning.put(jobId, new JobRecord(job, future));
}
}
@@ -236,32 +232,26 @@ public class FileOperationService extends Service implements Job.Listener {
*/
@GuardedBy("mRunning")
private @Nullable Job createJob(
@OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
DocumentStack stack) {
String id, ClipDetails details, DocumentStack stack) {
if (srcs.isEmpty()) {
Log.w(TAG, "Ignoring job request with empty srcs list. Id: " + id);
return null;
}
assert(details.getItemCount() > 0);
if (mRunning.containsKey(id)) {
Log.w(TAG, "Duplicate job id: " + id
+ ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
+ ". Ignoring job request for details: " + details + ", stack: " + stack + ".");
return null;
}
switch (operationType) {
switch (details.getOpType()) {
case OPERATION_COPY:
return jobFactory.createCopy(
this, getApplicationContext(), this, id, stack, srcs);
this, getApplicationContext(), this, id, stack, details);
case OPERATION_MOVE:
return jobFactory.createMove(
this, getApplicationContext(), this, id, stack, srcs,
srcParent);
this, getApplicationContext(), this, id, stack, details);
case OPERATION_DELETE:
return jobFactory.createDelete(
this, getApplicationContext(), this, id, stack, srcs,
srcParent);
this, getApplicationContext(), this, id, stack, details);
default:
throw new UnsupportedOperationException();
}
@@ -341,7 +331,7 @@ public class FileOperationService extends Service implements Job.Listener {
mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
if (job.hasFailures()) {
Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
Log.e(TAG, "Job failed on files: " + job.failedFileCount + ".");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
}
@@ -376,7 +366,6 @@ public class FileOperationService extends Service implements Job.Listener {
* we poll states of jobs.
*/
private static final class JobMonitor implements Runnable {
private static final long INITIAL_PROGRESS_DELAY_MILLIS = 10L;
private static final long PROGRESS_INTERVAL_MILLIS = 500L;
private final Job mJob;
@@ -390,8 +379,7 @@ public class FileOperationService extends Service implements Job.Listener {
}
private void start() {
// Delay the first update to avoid dividing by 0 when calculate speed
mHandler.postDelayed(this, INITIAL_PROGRESS_DELAY_MILLIS);
mHandler.post(this);
}
@Override
@@ -402,8 +390,11 @@ public class FileOperationService extends Service implements Job.Listener {
return;
}
mNotificationManager.notify(
mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
// Only job in set up state has progress bar
if (mJob.getState() == Job.STATE_SET_UP) {
mNotificationManager.notify(
mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
}
mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
}

View File

@@ -17,17 +17,12 @@
package com.android.documentsui.services;
import static android.os.SystemClock.elapsedRealtime;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.EXTRA_STACK;
import static com.android.documentsui.Shared.asArrayList;
import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_PARENT;
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import static com.android.documentsui.services.FileOperationService.EXTRA_CLIP_DETAILS;
import android.annotation.IntDef;
import android.app.Activity;
@@ -37,13 +32,12 @@ import android.os.Parcelable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Helper functions for starting various file operations.
@@ -63,41 +57,20 @@ public final class FileOperations {
/**
* Tries to start the activity. Returns the job id.
*/
public static String start(Context context, List<DocumentInfo> srcDocs, DocumentStack stack,
@OpType int operationType, Callback callback) {
public static String start(Context context, ClipDetails details,
DocumentStack stack, Callback callback) {
if (DEBUG) Log.d(TAG, "Handling generic 'start' call.");
switch (operationType) {
case OPERATION_COPY:
return FileOperations.copy(context, srcDocs, stack, callback);
case OPERATION_MOVE:
throw new IllegalArgumentException("Moving requires providing the source parent.");
case OPERATION_DELETE:
throw new UnsupportedOperationException("Delete isn't currently supported.");
default:
throw new UnsupportedOperationException("Unknown operation: " + operationType);
}
}
String jobId = createJobId();
Intent intent = createBaseIntent(context, jobId, details, stack);
/**
* Tries to start the activity. Returns the job id.
*/
public static String start(Context context, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
DocumentStack stack, @OpType int operationType, Callback callback) {
callback.onOperationResult(
Callback.STATUS_ACCEPTED, details.getOpType(), details.getItemCount());
if (DEBUG) Log.d(TAG, "Handling generic 'start' call.");
context.startService(intent);
switch (operationType) {
case OPERATION_COPY:
return FileOperations.copy(context, srcDocs, stack, callback);
case OPERATION_MOVE:
return FileOperations.move(context, srcDocs, srcParent, stack, callback);
case OPERATION_DELETE:
throw new UnsupportedOperationException("Delete isn't currently supported.");
default:
throw new UnsupportedOperationException("Unknown operation: " + operationType);
}
return jobId;
}
@VisibleForTesting
@@ -111,107 +84,22 @@ public final class FileOperations {
activity.startService(intent);
}
@VisibleForTesting
public static String copy(Context context, List<DocumentInfo> srcDocs,
DocumentStack destination, Callback callback) {
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'copy' operation id: " + jobId);
Intent intent = createBaseIntent(OPERATION_COPY, context, jobId, srcDocs, destination);
callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_COPY, srcDocs.size());
context.startService(intent);
return jobId;
}
/**
* Starts the service for a move operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @param srcParent Parent of all the source documents.
* @param destination The move destination stack.
*/
public static String move(Context context, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
DocumentStack destination, Callback callback) {
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'move' operation id: " + jobId);
Intent intent = createBaseIntent(OPERATION_MOVE, context, jobId, srcDocs, srcParent,
destination);
callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_MOVE, srcDocs.size());
context.startService(intent);
return jobId;
}
/**
* Starts the service for a delete operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to delete.
* @param srcParent Parent of all the source documents.
* @return Id of the job.
*/
public static String delete(
Activity activity, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
DocumentStack location) {
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id " + jobId + ".");
Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, srcParent,
location);
activity.startService(intent);
return jobId;
}
/**
* Starts the service for an operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files for an operation.
* @param details the clip details that contains source files and their parent
* @return Id of the job.
*/
public static Intent createBaseIntent(
@OpType int operationType, Context context, String jobId, List<DocumentInfo> srcDocs,
Context context, String jobId, ClipDetails details,
DocumentStack localeStack) {
Intent intent = new Intent(context, FileOperationService.class);
intent.putExtra(EXTRA_JOB_ID, jobId);
intent.putParcelableArrayListExtra(EXTRA_SRC_LIST, asArrayList(srcDocs));
intent.putExtra(EXTRA_CLIP_DETAILS, details);
intent.putExtra(EXTRA_STACK, (Parcelable) localeStack);
intent.putExtra(EXTRA_OPERATION, operationType);
return intent;
}
/**
* Starts the service for an operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @param srcParent Parent of all the source documents.
* @return Id of the job.
*/
public static Intent createBaseIntent(
@OpType int operationType, Context context, String jobId,
List<DocumentInfo> srcDocs, DocumentInfo srcParent, DocumentStack localeStack) {
Intent intent = new Intent(context, FileOperationService.class);
intent.putExtra(EXTRA_JOB_ID, jobId);
intent.putParcelableArrayListExtra(EXTRA_SRC_LIST, asArrayList(srcDocs));
intent.putExtra(EXTRA_SRC_PARENT, srcParent);
intent.putExtra(EXTRA_STACK, (Parcelable) localeStack);
intent.putExtra(EXTRA_OPERATION, operationType);
return intent;
}

View File

@@ -22,6 +22,9 @@ import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG
import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
import android.annotation.DrawableRes;
@@ -40,6 +43,7 @@ import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.util.Log;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.FilesActivity;
import com.android.documentsui.Metrics;
import com.android.documentsui.OperationDialogFragment;
@@ -53,7 +57,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -64,16 +67,17 @@ abstract public class Job implements Runnable {
private static final String TAG = "Job";
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CREATED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED})
@IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
@interface State {}
static final int STATE_CREATED = 0;
static final int STATE_STARTED = 1;
static final int STATE_COMPLETED = 2;
static final int STATE_SET_UP = 2;
static final int STATE_COMPLETED = 3;
/**
* A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
* completed.
*/
static final int STATE_CANCELED = 3;
static final int STATE_CANCELED = 4;
static final String INTENT_TAG_WARNING = "warning";
static final String INTENT_TAG_FAILURE = "failure";
@@ -87,7 +91,9 @@ abstract public class Job implements Runnable {
final @OpType int operationType;
final String id;
final DocumentStack stack;
final ClipDetails details;
int failedFileCount = 0;
final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
final Notification.Builder mProgressBuilder;
@@ -97,8 +103,6 @@ abstract public class Job implements Runnable {
/**
* A simple progressable job, much like an AsyncTask, but with support
* for providing various related notification, progress and navigation information.
* @param operationType
*
* @param service The service context in which this job is running.
* @param appContext The context of the invoking application. This is usually
* just {@code getApplicationContext()}.
@@ -107,19 +111,21 @@ abstract public class Job implements Runnable {
* @param stack The documents stack context relating to this request. This is the
* destination in the Files app where the user will be take when the
* navigation intent is invoked (presumably from notification).
* @param details details that contains {@link FileOperationService.OpType}
*/
Job(Context service, Context appContext, Listener listener,
@OpType int operationType, String id, DocumentStack stack) {
String id, DocumentStack stack, ClipDetails details) {
assert(operationType != OPERATION_UNKNOWN);
assert(details.getOpType() != OPERATION_UNKNOWN);
this.service = service;
this.appContext = appContext;
this.listener = listener;
this.operationType = operationType;
this.operationType = details.getOpType();
this.id = id;
this.stack = stack;
this.details = details;
mProgressBuilder = createProgressBuilder();
}
@@ -134,18 +140,29 @@ abstract public class Job implements Runnable {
mState = STATE_STARTED;
listener.onStart(this);
try {
start();
boolean result = setUp();
if (result && !isCanceled()) {
mState = STATE_SET_UP;
start();
}
} catch (RuntimeException e) {
// No exceptions should be thrown here, as all calls to the provider must be
// handled within Job implementations. However, just in case catch them here.
Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Metrics.logFileOperationErrors(service, operationType, failedFiles);
} finally {
mState = (mState == STATE_STARTED) ? STATE_COMPLETED : mState;
mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
listener.onFinished(this);
// NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
// at this point, user won't be able to paste it to anywhere else because the underlying
details.dispose(appContext);
}
}
boolean setUp() {
return true;
}
abstract void start();
abstract Notification getSetupNotification();
@@ -201,11 +218,12 @@ abstract public class Job implements Runnable {
}
void onFileFailed(DocumentInfo file) {
++failedFileCount;
failedFiles.add(file);
}
final boolean hasFailures() {
return !failedFiles.isEmpty();
return failedFileCount > 0;
}
boolean hasWarnings() {
@@ -242,7 +260,7 @@ abstract public class Job implements Runnable {
final Notification.Builder errorBuilder = new Notification.Builder(service)
.setContentTitle(service.getResources().getQuantityString(titleId,
failedFiles.size(), failedFiles.size()))
failedFileCount, failedFileCount))
.setContentText(service.getString(R.string.notification_touch_for_details))
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
@@ -319,28 +337,29 @@ abstract public class Job implements Runnable {
static final Factory instance = new Factory();
Job createCopy(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs) {
assert(!srcs.isEmpty());
String id, DocumentStack stack, ClipDetails details) {
assert(details.getOpType() == OPERATION_COPY);
assert(details.getItemCount() > 0);
assert(stack.peek().isCreateSupported());
return new CopyJob(service, appContext, listener, id, stack, srcs);
return new CopyJob(service, appContext, listener, id, stack, details);
}
Job createMove(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs,
DocumentInfo srcParent) {
assert(!srcs.isEmpty());
String id, DocumentStack stack, ClipDetails details) {
assert(details.getOpType() == OPERATION_MOVE);
assert(details.getItemCount() > 0);
assert(stack.peek().isCreateSupported());
return new MoveJob(service, appContext, listener, id, stack, srcs, srcParent);
return new MoveJob(service, appContext, listener, id, stack, details);
}
Job createDelete(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs,
DocumentInfo srcParent) {
assert(!srcs.isEmpty());
String id, DocumentStack stack, ClipDetails details) {
assert(details.getOpType() == OPERATION_DELETE);
assert(details.getItemCount() > 0);
// stack is empty if we delete docs from recent.
// we can't currently delete from archives.
assert(stack.isEmpty() || stack.peek().isDirectory());
return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent);
return new DeleteJob(service, appContext, listener, id, stack, details);
}
}

View File

@@ -17,28 +17,29 @@
package com.android.documentsui.services;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.ContentResolver;
import android.content.Context;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
import java.io.FileNotFoundException;
// TODO: Stop extending CopyJob.
final class MoveJob extends CopyJob {
private static final String TAG = "MoveJob";
final DocumentInfo mSrcParent;
DocumentInfo mSrcParent;
/**
* Moves files to a destination identified by {@code destination}.
@@ -47,13 +48,11 @@ final class MoveJob extends CopyJob {
*
* @see @link {@link Job} constructor for most param descriptions.
*
* @param srcs List of files to be moved.
* @param srcParent Parent of all source files.
* @param details {@link ClipDetails} that contains list of files to be moved and their parent
*/
MoveJob(Context service, Context appContext, Listener listener,
String id, DocumentStack destination, List<DocumentInfo> srcs, DocumentInfo srcParent) {
super(service, appContext, listener, OPERATION_MOVE, id, destination, srcs);
this.mSrcParent = srcParent;
String id, DocumentStack destination, ClipDetails details) {
super(service, appContext, listener, id, destination, details);
}
@Override
@@ -81,6 +80,20 @@ final class MoveJob extends CopyJob {
R.plurals.move_error_notification_title, R.drawable.ic_menu_copy);
}
@Override
public void start() {
final ContentResolver resolver = appContext.getContentResolver();
try {
mSrcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
} catch(FileNotFoundException e) {
Log.e(TAG, "Failed to create srcParent.", e);
failedFileCount += details.getItemCount();
return;
}
super.start();
}
void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)
throws ResourceException {

View File

@@ -0,0 +1,163 @@
/*
* 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.DocumentsContract;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.testing.TestScheduledExecutorService;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ClipDetailsTest {
private static final String AUTHORITY = "foo";
private static final @OpType int OP_TYPE = FileOperationService.OPERATION_COPY;
private static final Uri SRC_PARENT =
DocumentsContract.buildDocumentUri(AUTHORITY, Integer.toString(0));
private static final List<Uri> SHORT_URI_LIST = createList(3);
private static final List<Uri> LONG_URI_LIST = createList(Shared.MAX_DOCS_IN_INTENT + 5);
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private TestScheduledExecutorService mExecutor;
private ClipStorage mStorage;
@Before
public void setUp() {
mExecutor = new TestScheduledExecutorService();
AsyncTask.setDefaultExecutor(mExecutor);
mStorage = new ClipStorage(folder.getRoot());
}
@AfterClass
public static void tearDownOnce() {
AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
}
@Test
public void testOpTypeEquals_shortList() {
ClipDetails details = createDetailsWithShortList();
assertEquals(OP_TYPE, details.getOpType());
}
@Test
public void testOpTypeEquals_longList() {
ClipDetails details = createDetailsWithLongList();
assertEquals(OP_TYPE, details.getOpType());
}
@Test
public void testItemCountEquals_shortList() {
ClipDetails details = createDetailsWithShortList();
assertEquals(SHORT_URI_LIST.size(), details.getItemCount());
}
@Test
public void testItemCountEquals_longList() {
ClipDetails details = createDetailsWithLongList();
assertEquals(LONG_URI_LIST.size(), details.getItemCount());
}
@Test
public void testGetDocsEquals_shortList() throws Exception {
ClipDetails details = createDetailsWithShortList();
assertIterableEquals(SHORT_URI_LIST, details.getDocs(mStorage));
}
@Test
public void testGetDocsEquals_longList() throws Exception {
ClipDetails details = createDetailsWithLongList();
assertIterableEquals(LONG_URI_LIST, details.getDocs(mStorage));
}
@Test
public void testDispose_shortList() throws Exception {
ClipDetails details = createDetailsWithShortList();
details.dispose(mStorage);
}
@Test
public void testDispose_longList() throws Exception {
ClipDetails details = createDetailsWithLongList();
details.dispose(mStorage);
}
private ClipDetails createDetailsWithShortList() {
return ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, SHORT_URI_LIST, mStorage);
}
private ClipDetails createDetailsWithLongList() {
ClipDetails details =
ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, LONG_URI_LIST, mStorage);
mExecutor.runAll();
return details;
}
private void assertIterableEquals(Iterable<Uri> expected, Iterable<Uri> value) {
Iterator<Uri> expectedIter = expected.iterator();
Iterator<Uri> valueIter = value.iterator();
while (expectedIter.hasNext() && valueIter.hasNext()) {
assertEquals(expectedIter.next(), valueIter.next());
}
assertFalse(expectedIter.hasNext());
assertFalse(expectedIter.hasNext());
}
private static List<Uri> createList(int count) {
List<Uri> uris = new ArrayList<>(count);
for (int i = 0; i < count; ++i) {
uris.add(DocumentsContract.buildDocumentUri(AUTHORITY, Integer.toString(i)));
}
return uris;
}
}

View File

@@ -21,23 +21,26 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.documentsui.ClipStorage.Writer;
import com.android.documentsui.ClipStorage.Reader;
import com.android.documentsui.dirlist.TestModel;
import com.android.documentsui.testing.TestScheduledExecutorService;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ClipStorageTest {
@@ -48,38 +51,52 @@ public class ClipStorageTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private TestScheduledExecutorService mExecutor;
private ClipStorage mStorage;
private TestModel mModel;
private long mTag;
@Before
public void setUp() {
File clipDir = ClipStorage.prepareStorage(folder.getRoot());
mStorage = new ClipStorage(clipDir);
mExecutor = new TestScheduledExecutorService();
AsyncTask.setDefaultExecutor(mExecutor);
mTag = mStorage.createTag();
}
@AfterClass
public static void tearDownOnce() {
AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
}
@Test
public void testWritePrimary() throws Exception {
Writer writer = mStorage.createWriter();
writeAll(TEST_URIS, writer);
public void testWrite() throws Exception {
writeAll(mTag, TEST_URIS);
}
@Test
public void testRead() throws Exception {
Writer writer = mStorage.createWriter();
writeAll(TEST_URIS, writer);
long tag = mStorage.savePrimary();
List<Uri> uris = mStorage.read(tag);
writeAll(mTag, TEST_URIS);
List<Uri> uris = new ArrayList<>();
try(Reader provider = mStorage.createReader(mTag)) {
for (Uri uri : provider) {
uris.add(uri);
}
}
assertEquals(TEST_URIS, uris);
}
@Test
public void testDelete() throws Exception {
Writer writer = mStorage.createWriter();
writeAll(TEST_URIS, writer);
long tag = mStorage.savePrimary();
mStorage.delete(tag);
writeAll(mTag, TEST_URIS);
mStorage.delete(mTag);
try {
mStorage.read(tag);
mStorage.createReader(mTag);
} catch (IOException expected) {}
}
@@ -91,21 +108,9 @@ public class ClipStorageTest {
assertFalse(clipDir.equals(folder.getRoot()));
}
@Test
public void testPrepareStorage_DeletesPreviousClipFiles() throws Exception {
File clipDir = ClipStorage.prepareStorage(folder.getRoot());
new File(clipDir, "somefakefile.poodles").createNewFile();
new File(clipDir, "yodles.yam").createNewFile();
assertEquals(2, clipDir.listFiles().length);
clipDir = ClipStorage.prepareStorage(folder.getRoot());
assertEquals(0, clipDir.listFiles().length);
}
private static void writeAll(List<Uri> uris, Writer writer) throws IOException {
for (Uri uri : uris) {
writer.write(uri);
}
private void writeAll(long tag, List<Uri> uris) {
new ClipStorage.PersistTask(mStorage, uris, tag).execute();
mExecutor.runAll();
}
private static List<Uri> createList(String... values) {

View File

@@ -16,6 +16,8 @@
package com.android.documentsui.services;
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
@@ -23,7 +25,6 @@ import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
@@ -110,7 +111,8 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
public void runNoCopyDirToSelfTest() throws Exception {
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
createJob(newArrayList(testDir),
createJob(OPERATION_COPY,
newArrayList(testDir),
DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
testDir).run();
@@ -125,7 +127,8 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
Uri destDir = mDocs.createFolder(testDir, "theDescendent");
createJob(newArrayList(testDir),
createJob(OPERATION_COPY,
newArrayList(testDir),
DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
destDir).run();
@@ -160,6 +163,6 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
final T createJob(List<Uri> srcs) throws Exception {
Uri srcParent = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
return createJob(srcs, srcParent, destination);
return createJob(OPERATION_COPY, srcs, srcParent, destination);
}
}

View File

@@ -24,17 +24,17 @@ import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.DocumentsProviderHelper;
import com.android.documentsui.StubProvider;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
import com.google.common.collect.Lists;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.testing.ClipDetailsFactory;
import java.util.List;
@@ -85,18 +85,16 @@ public abstract class AbstractJobTest<T extends Job> extends AndroidTestCase {
mDestRoot = mDocs.getRoot(ROOT_1_ID);
}
final T createJob(List<Uri> srcs, Uri srcParent, Uri destination) throws Exception {
final T createJob(@OpType int opType, List<Uri> srcs, Uri srcParent, Uri destination)
throws Exception {
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, destination));
stack.root = mSrcRoot;
List<DocumentInfo> srcDocs = Lists.newArrayList();
for (Uri src : srcs) {
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
}
return createJob(srcDocs, DocumentInfo.fromUri(mResolver, srcParent), stack);
ClipDetails details = ClipDetailsFactory.createClipDetails(opType, srcParent, srcs);
return createJob(details, stack);
}
abstract T createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack destination)
abstract T createJob(ClipDetails details, DocumentStack destination)
throws Exception;
}

View File

@@ -22,11 +22,9 @@ import android.net.Uri;
import android.provider.DocumentsContract.Document;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
@MediumTest
public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
@@ -78,10 +76,9 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
}
@Override
// TODO: Stop passing srcParent here, as it's not used for copying.
CopyJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
CopyJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new CopyJob(
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}

View File

@@ -16,13 +16,15 @@
package com.android.documentsui.services;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
@@ -49,15 +51,14 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
*/
private final DeleteJob createJob(List<Uri> srcs, Uri srcParent) throws Exception {
Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
return createJob(srcs, srcParent, stack);
return createJob(OPERATION_DELETE, srcs, srcParent, stack);
}
@Override
// TODO: Remove inheritance, as stack is not used for deleting, nor srcParent.
DeleteJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
@Override
DeleteJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new DeleteJob(
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs,
srcParent);
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}

View File

@@ -29,10 +29,13 @@ import android.net.Uri;
import android.test.ServiceTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.Job.Listener;
import com.android.documentsui.testing.ClipDetailsFactory;
import com.android.documentsui.testing.TestHandler;
import com.android.documentsui.testing.TestScheduledExecutorService;
import java.util.ArrayList;
import java.util.List;
@@ -40,6 +43,8 @@ import java.util.List;
@MediumTest
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {
private static final Uri SRC_PARENT =
Uri.parse("content://com.android.documentsui.testing/parent");
private static final DocumentInfo ALPHA_DOC = createDoc("alpha");
private static final DocumentInfo BETA_DOC = createDoc("alpha");
private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
@@ -90,7 +95,11 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
}
public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception {
startService(createCopyIntent(new ArrayList<DocumentInfo>(), BETA_DOC));
try {
startService(createCopyIntent(new ArrayList<>(), BETA_DOC));
} catch(AssertionError e) {
// Expected AssertionError
}
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
mJobFactory.assertJobsCreated(1);
@@ -219,13 +228,29 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
DocumentStack stack = new DocumentStack();
stack.push(dest);
return createBaseIntent(OPERATION_COPY, getContext(), createJobId(), files, stack);
List<Uri> uris = new ArrayList<>(files.size());
for (DocumentInfo file: files) {
uris.add(file.derivedUri);
}
ClipDetails details =
ClipDetailsFactory.createClipDetails(OPERATION_COPY, SRC_PARENT, uris);
return createBaseIntent(getContext(), createJobId(), details, stack);
}
private Intent createDeleteIntent(ArrayList<DocumentInfo> files) {
DocumentStack stack = new DocumentStack();
return createBaseIntent(OPERATION_DELETE, getContext(), createJobId(), files, stack);
List<Uri> uris = new ArrayList<>(files.size());
for (DocumentInfo file: files) {
uris.add(file.derivedUri);
}
ClipDetails details =
ClipDetailsFactory.createClipDetails(OPERATION_DELETE, SRC_PARENT, uris);
return createBaseIntent(getContext(), createJobId(), details, stack);
}
private static DocumentInfo createDoc(String name) {
@@ -291,28 +316,28 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
@Override
Job createCopy(Context service, Context appContext, Listener listener, String id,
DocumentStack stack, List<DocumentInfo> srcs) {
DocumentStack stack, ClipDetails details) {
if (srcs.isEmpty()) {
if (details.getItemCount() == 0) {
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(
service, appContext, listener, OPERATION_COPY, id, stack, mJobRunnable);
service, appContext, listener, id, stack, details, mJobRunnable);
copyJobs.add(job);
return job;
}
@Override
Job createDelete(Context service, Context appContext, Listener listener, String id,
DocumentStack stack, List<DocumentInfo> srcs, DocumentInfo srcParent) {
DocumentStack stack, ClipDetails details) {
if (srcs.isEmpty()) {
if (details.getItemCount() == 0) {
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(
service, appContext, listener, OPERATION_DELETE, id, stack, mJobRunnable);
service, appContext, listener, id, stack, details, mJobRunnable);
deleteJobs.add(job);
return job;

View File

@@ -20,14 +20,11 @@ import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
@MediumTest
public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
@@ -110,10 +107,9 @@ public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
// TODO: Add test cases for moving when multi-parented.
@Override
MoveJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
MoveJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new MoveJob(
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs,
srcParent);
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}

View File

@@ -22,12 +22,14 @@ import static junit.framework.Assert.assertTrue;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
import android.icu.text.NumberFormat;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import java.text.NumberFormat;
public class TestJob extends Job {
private boolean mStarted;
@@ -37,8 +39,8 @@ public class TestJob extends Job {
TestJob(
Context service, Context appContext, Listener listener,
int operationType, String id, DocumentStack stack, Runnable startRunnable) {
super(service, appContext, listener, operationType, id, stack);
String id, DocumentStack stack, ClipDetails details, Runnable startRunnable) {
super(service, appContext, listener, id, stack, details);
mStartRunnable = startRunnable;
}

View File

@@ -0,0 +1,32 @@
/*
* 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.testing;
import android.net.Uri;
import com.android.documentsui.ClipDetails;
import com.android.documentsui.services.FileOperationService.OpType;
import java.util.List;
public final class ClipDetailsFactory {
private ClipDetailsFactory() {}
public static ClipDetails createClipDetails(@OpType int opType, Uri srcParent, List<Uri> docs) {
return new ClipDetails.StandardClipDetails(opType, srcParent, docs);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.documentsui.services;
package com.android.documentsui.testing;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.fail;
@@ -47,7 +47,7 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
return new ArrayList<>();
}
void assertShutdown() {
public void assertShutdown() {
if (!shutdown) {
fail("Executor wasn't shut down.");
}
@@ -109,7 +109,7 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
@Override
public void execute(Runnable command) {
throw new UnsupportedOperationException();
schedule(command, 0, TimeUnit.MILLISECONDS);
}
@Override
@@ -136,13 +136,13 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
throw new UnsupportedOperationException();
}
void runAll() {
public void runAll() {
for (TestFuture future : scheduled) {
future.runnable.run();
}
}
void run(int taskIndex) {
public void run(int taskIndex) {
scheduled.get(taskIndex).runnable.run();
}