[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:
committed by
Garfield Tan
parent
42e62608e5
commit
f46958bebd
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user