diff options
author | 2016-06-28 17:17:38 -0700 | |
---|---|---|
committer | 2016-06-29 17:05:43 -0700 | |
commit | 4833477d7d42fa79ee42956bae4aebad77074e4b (patch) | |
tree | 2bb7472b41535bf7f39ed7bd8bc89b36f41dd73c | |
parent | ba1cb3dd29f189eda3f78f8c4eec4e780d2f54f4 (diff) |
[multi-part] Eliminate 1k selection limit
* Rename class ClipDetails.
* Add FileOperation class and merge it with JobFactory.
Bug: 28194201
Change-Id: I46639b21c9e424644289a1bf34b85234a9becd2b
25 files changed, 853 insertions, 701 deletions
diff --git a/src/com/android/documentsui/ClipDetails.java b/src/com/android/documentsui/ClipDetails.java deleted file mode 100644 index 6cd035376..000000000 --- a/src/com/android/documentsui/ClipDetails.java +++ /dev/null @@ -1,343 +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; - -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]; - } - }; - } -} diff --git a/src/com/android/documentsui/DocumentClipper.java b/src/com/android/documentsui/DocumentClipper.java index 4c103c42c..72413bdcb 100644 --- a/src/com/android/documentsui/DocumentClipper.java +++ b/src/com/android/documentsui/DocumentClipper.java @@ -32,6 +32,7 @@ import android.util.Log; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; +import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; import com.android.documentsui.services.FileOperations; @@ -349,28 +350,35 @@ public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChan return; } - ClipDetails details = ClipDetails.createClipDetails(clipData); + PersistableBundle bundle = clipData.getDescription().getExtras(); + @OpType int opType = getOpType(bundle); + UrisSupplier uris = UrisSupplier.create(clipData); if (!canCopy(destination)) { callback.onOperationResult( - FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0); + FileOperations.Callback.STATUS_REJECTED, opType, 0); return; } - if (details.getItemCount() == 0) { + if (uris.getItemCount() == 0) { callback.onOperationResult( - FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 0); + FileOperations.Callback.STATUS_ACCEPTED, opType, 0); return; } - DocumentStack dstStack = new DocumentStack(); - dstStack.push(destination); - dstStack.addAll(docStack); + DocumentStack dstStack = new DocumentStack(docStack, destination); - // Pass root here so that we can perform "download" root check when - dstStack.root = docStack.root; + String srcParentString = bundle.getString(SRC_PARENT_KEY); + Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString); - FileOperations.start(mContext, details, dstStack, callback); + FileOperation operation = new FileOperation.Builder() + .withOpType(opType) + .withSrcParent(srcParent) + .withDestination(dstStack) + .withSrcs(uris) + .build(); + + FileOperations.start(mContext, operation, callback); } /** @@ -399,8 +407,24 @@ public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChan } ClipDescription description = data.getDescription(); + if (description == null) { + return ClipStorage.NO_SELECTION_TAG; + } + BaseBundle bundle = description.getExtras(); + if (bundle == null) { + return ClipStorage.NO_SELECTION_TAG; + } + return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG); } + public static @OpType int getOpType(ClipData data) { + PersistableBundle bundle = data.getDescription().getExtras(); + return getOpType(bundle); + } + + private static @OpType int getOpType(PersistableBundle bundle) { + return bundle.getInt(OP_TYPE_KEY); + } } diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java index b8559bc19..0a518cd20 100644 --- a/src/com/android/documentsui/DocumentsActivity.java +++ b/src/com/android/documentsui/DocumentsActivity.java @@ -41,7 +41,6 @@ import android.provider.DocumentsContract; import android.support.design.widget.Snackbar; import android.util.Log; import android.view.Menu; -import android.view.MenuItem; import com.android.documentsui.MenuManager.DirectoryDetails; import com.android.documentsui.RecentsProvider.RecentColumns; @@ -156,7 +155,7 @@ public class DocumentsActivity extends BaseActivity { state.directoryCopy = intent.getBooleanExtra( Shared.EXTRA_DIRECTORY_COPY, false); state.copyOperationSubType = intent.getIntExtra( - FileOperationService.EXTRA_OPERATION, + FileOperationService.EXTRA_OPERATION_TYPE, FileOperationService.OPERATION_COPY); } } @@ -386,7 +385,7 @@ public class DocumentsActivity extends BaseActivity { // Picking a copy destination is only used internally by us, so we // don't need to extend permissions to the caller. intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); - intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType); + intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); } else { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java index f82bdf1ad..1edfffe25 100644 --- a/src/com/android/documentsui/FilesActivity.java +++ b/src/com/android/documentsui/FilesActivity.java @@ -139,7 +139,7 @@ public class FilesActivity extends BaseActivity { // Only show it manually for the first time (icicle is null). if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) { final int opType = intent.getIntExtra( - FileOperationService.EXTRA_OPERATION, + FileOperationService.EXTRA_OPERATION_TYPE, FileOperationService.OPERATION_COPY); final ArrayList<DocumentInfo> srcList = intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST); diff --git a/src/com/android/documentsui/OperationDialogFragment.java b/src/com/android/documentsui/OperationDialogFragment.java index 9a3f7a83e..140baad03 100644 --- a/src/com/android/documentsui/OperationDialogFragment.java +++ b/src/com/android/documentsui/OperationDialogFragment.java @@ -60,7 +60,7 @@ public class OperationDialogFragment extends DialogFragment { @OpType int operationType) { final Bundle args = new Bundle(); args.putInt(FileOperationService.EXTRA_DIALOG_TYPE, dialogType); - args.putInt(FileOperationService.EXTRA_OPERATION, operationType); + args.putInt(FileOperationService.EXTRA_OPERATION_TYPE, operationType); args.putParcelableArrayList(FileOperationService.EXTRA_SRC_LIST, failedSrcList); final FragmentTransaction ft = fm.beginTransaction(); @@ -78,7 +78,7 @@ public class OperationDialogFragment extends DialogFragment { final @DialogType int dialogType = getArguments().getInt(FileOperationService.EXTRA_DIALOG_TYPE); final @OpType int operationType = - getArguments().getInt(FileOperationService.EXTRA_OPERATION); + getArguments().getInt(FileOperationService.EXTRA_OPERATION_TYPE); final ArrayList<DocumentInfo> srcList = getArguments().getParcelableArrayList( FileOperationService.EXTRA_SRC_LIST); diff --git a/src/com/android/documentsui/UrisSupplier.java b/src/com/android/documentsui/UrisSupplier.java new file mode 100644 index 000000000..c5d30aacf --- /dev/null +++ b/src/com/android/documentsui/UrisSupplier.java @@ -0,0 +1,275 @@ +/* + * 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 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.FileOperation; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * UrisSupplier provides doc uri list to {@link FileOperation}. + * + * <p>Under the hood it provides cross-process synchronization support such that its consumer doesn't + * need to explicitly synchronize its access. + */ +public abstract class UrisSupplier implements Parcelable { + + public abstract int getItemCount(); + + /** + * Gets doc list. This may only be called once because it may read a file + * to get the list. + * + * @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel. + */ + public Iterable<Uri> getDocs(Context context) throws IOException { + return getDocs(DocumentsApplication.getClipStorage(context)); + } + + @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) {} + + @Override + public int describeContents() { + return 0; + } + + public static UrisSupplier create(ClipData clipData) { + UrisSupplier uris; + PersistableBundle bundle = clipData.getDescription().getExtras(); + if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) { + uris = new JumboUrisSupplier(clipData); + } else { + uris = new StandardUrisSupplier(clipData); + } + + return uris; + } + + public static UrisSupplier create( + 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 create(uris, storage); + } + + @VisibleForTesting + static UrisSupplier create(List<Uri> uris, ClipStorage storage) { + UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT) + ? new JumboUrisSupplier(uris, storage) + : new StandardUrisSupplier(uris); + + return urisSupplier; + } + + private static class JumboUrisSupplier extends UrisSupplier { + private static final String TAG = "JumboUrisSupplier"; + + private final long mSelectionTag; + private final int mSelectionSize; + + private final transient AtomicReference<ClipStorage.Reader> mReader = + new AtomicReference<>(); + + private JumboUrisSupplier(ClipData 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 JumboUrisSupplier(Collection<Uri> uris, ClipStorage storage) { + mSelectionTag = storage.createTag(); + new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute(); + mSelectionSize = uris.size(); + } + + @Override + public int getItemCount() { + return mSelectionSize; + } + + @Override + Iterable<Uri> getDocs(ClipStorage storage) throws IOException { + ClipStorage.Reader reader = mReader.getAndSet(storage.createReader(mSelectionTag)); + if (reader != null) { + reader.close(); + mReader.get().close(); + throw new IllegalStateException("This method can only be called once."); + } + + return mReader.get(); + } + + @Override + void dispose(ClipStorage storage) { + try { + ClipStorage.Reader reader = mReader.get(); + if (reader != null) { + reader.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("JumboUrisSupplier{"); + builder.append("selectionTag=").append(mSelectionTag); + builder.append(", selectionSize=").append(mSelectionSize); + builder.append("}"); + return builder.toString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mSelectionTag); + dest.writeInt(mSelectionSize); + } + + private JumboUrisSupplier(Parcel in) { + mSelectionTag = in.readLong(); + mSelectionSize = in.readInt(); + } + + public static final Parcelable.Creator<JumboUrisSupplier> CREATOR = + new Parcelable.Creator<JumboUrisSupplier>() { + + @Override + public JumboUrisSupplier createFromParcel(Parcel source) { + return new JumboUrisSupplier(source); + } + + @Override + public JumboUrisSupplier[] newArray(int size) { + return new JumboUrisSupplier[size]; + } + }; + } + + /** + * This class and its constructor is visible for testing to create test doubles of + * {@link UrisSupplier}. + */ + @VisibleForTesting + public static class StandardUrisSupplier extends UrisSupplier { + private final List<Uri> mDocs; + + private StandardUrisSupplier(ClipData clipData) { + mDocs = listDocs(clipData); + } + + @VisibleForTesting + public StandardUrisSupplier(List<Uri> docs) { + 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 + Iterable<Uri> getDocs(ClipStorage storage) { + return mDocs; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("StandardUrisSupplier{"); + builder.append("docs=").append(mDocs.toString()); + builder.append("}"); + return builder.toString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(mDocs); + } + + private StandardUrisSupplier(Parcel in) { + mDocs = in.createTypedArrayList(Uri.CREATOR); + } + + public static final Parcelable.Creator<StandardUrisSupplier> CREATOR = + new Parcelable.Creator<StandardUrisSupplier>() { + + @Override + public StandardUrisSupplier createFromParcel(Parcel source) { + return new StandardUrisSupplier(source); + } + + @Override + public StandardUrisSupplier[] newArray(int size) { + return new StandardUrisSupplier[size]; + } + }; + } +} diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index f96341a24..86c6c9944 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -72,9 +72,9 @@ 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.UrisSupplier; import com.android.documentsui.DocumentClipper; import com.android.documentsui.DocumentsActivity; import com.android.documentsui.DocumentsApplication; @@ -97,6 +97,7 @@ import com.android.documentsui.State.ViewMode; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.RootInfo; +import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; import com.android.documentsui.services.FileOperations; @@ -172,7 +173,7 @@ public class DirectoryFragment extends Fragment 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 @Nullable FileOperation mPendingOperation; private boolean mSearchMode = false; private @Nullable BandController mBandController; @@ -269,7 +270,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); + mPendingOperation = args.getParcelable(FileOperationService.EXTRA_OPERATION); // Restore any selection we may have squirreled away in retained state. @Nullable RetainedState retained = getBaseActivity().getRetainedState(); @@ -359,7 +360,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); + outState.putParcelable(FileOperationService.EXTRA_OPERATION, mPendingOperation); } @Override @@ -400,21 +401,19 @@ public class DirectoryFragment extends Fragment private void handleCopyResult(int resultCode, Intent data) { - ClipDetails details = mDetailsForCopy; - mDetailsForCopy = null; + FileOperation operation = mPendingOperation; + mPendingOperation = 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()); + operation.dispose(getContext()); return; } - FileOperations.start( - getContext(), - details, - data.getParcelableExtra(Shared.EXTRA_STACK), - mFileOpCallback); + operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK)); + + FileOperations.start(getContext(), operation, mFileOpCallback); } protected boolean onDoubleTap(MotionInputEvent event) { @@ -1010,14 +1009,19 @@ public class DirectoryFragment extends Fragment Log.w(TAG, "Action mode is null before deleting documents."); } - ClipDetails details = ClipDetails.createClipDetails( - FileOperationService.OPERATION_DELETE, - srcParent.derivedUri, + UrisSupplier srcs = UrisSupplier.create( selected, mModel::getItemUri, getContext()); - FileOperations.start(getActivity(), details, - getDisplayState().stack, mFileOpCallback); + + FileOperation operation = new FileOperation.Builder() + .withOpType(FileOperationService.OPERATION_DELETE) + .withDestination(getDisplayState().stack) + .withSrcs(srcs) + .withSrcParent(srcParent.derivedUri) + .build(); + + FileOperations.start(getActivity(), operation, mFileOpCallback); } }) .setNegativeButton(android.R.string.cancel, null) @@ -1041,9 +1045,15 @@ public class DirectoryFragment extends Fragment getActivity(), DocumentsActivity.class); + UrisSupplier srcs = + UrisSupplier.create(selected, mModel::getItemUri, getContext()); + Uri srcParent = getDisplayState().stack.peek().derivedUri; - mDetailsForCopy = ClipDetails.createClipDetails( - mode, srcParent, selected, mModel::getItemUri, getContext()); + mPendingOperation = new FileOperation.Builder() + .withOpType(mode) + .withSrcParent(srcParent) + .withSrcs(srcs) + .build(); // Relay any config overrides bits present in the original intent. Intent original = getActivity().getIntent(); @@ -1068,6 +1078,7 @@ public class DirectoryFragment extends Fragment // (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_TYPE, mode); // This just identifies the type of request...we'll check it // when we reveive a response. @@ -1304,8 +1315,7 @@ public class DirectoryFragment extends Fragment ClipData clipData = event.getClipData(); assert (clipData != null); - assert(ClipDetails.createClipDetails(clipData).getOpType() - == FileOperationService.OPERATION_COPY); + assert(DocumentClipper.getOpType(clipData) == 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 diff --git a/src/com/android/documentsui/model/DocumentStack.java b/src/com/android/documentsui/model/DocumentStack.java index 34bd69627..c4f4dc18a 100644 --- a/src/com/android/documentsui/model/DocumentStack.java +++ b/src/com/android/documentsui/model/DocumentStack.java @@ -39,6 +39,24 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable, public RootInfo root; + public DocumentStack() {}; + + /** + * Makes a new copy, and pushes all docs to the new copy in the same order as they're passed + * as parameters, i.e. the last document will be at the top of the stack. + * + * @param src + * @param docs + */ + public DocumentStack(DocumentStack src, DocumentInfo... docs) { + super(src); + for (DocumentInfo doc : docs) { + push(doc); + } + + root = src.root; + } + public String getTitle() { if (size() == 1 && root != null) { return root.title; diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java index fac8667ae..390656cf8 100644 --- a/src/com/android/documentsui/services/CopyJob.java +++ b/src/com/android/documentsui/services/CopyJob.java @@ -27,8 +27,9 @@ import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.model.DocumentInfo.getCursorLong; 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_OPERATION_TYPE; 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; @@ -50,12 +51,13 @@ import android.text.format.DateUtils; import android.util.Log; import android.webkit.MimeTypeMap; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; 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.model.RootInfo; +import com.android.documentsui.services.FileOperationService.OpType; import libcore.io.IoUtils; @@ -85,17 +87,20 @@ class CopyJob extends Job { /** * @see @link {@link Job} constructor for most param descriptions. - * - * @param details clip details containing source file list */ - CopyJob(Context service, Context appContext, Listener listener, - String id, DocumentStack destination, ClipDetails details) { - super(service, appContext, listener, id, destination, details); + CopyJob(Context service, Listener listener, String id, DocumentStack destination, + UrisSupplier srcs) { + this(service, listener, id, OPERATION_COPY, destination, srcs); + } + + CopyJob(Context service, Listener listener, String id, @OpType int opType, + DocumentStack destination, UrisSupplier srcs) { + super(service, listener, id, opType, destination, srcs); - assert(details.getItemCount() > 0); + assert(srcs.getItemCount() > 0); // delay the initialization of it to setUp() because it may be IO extensive. - mSrcs = new ArrayList<>(details.getItemCount()); + mSrcs = new ArrayList<>(srcs.getItemCount()); } @Override @@ -184,7 +189,7 @@ class CopyJob extends Job { Notification getWarningNotification() { final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING); navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED); - navigateIntent.putExtra(EXTRA_OPERATION, operationType); + navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType); navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles); @@ -257,7 +262,7 @@ class CopyJob extends Job { private void buildDocumentList() throws ResourceException { try { final ContentResolver resolver = appContext.getContentResolver(); - final Iterable<Uri> uris = details.getDocs(appContext); + final Iterable<Uri> uris = srcs.getDocs(appContext); for (Uri uri : uris) { DocumentInfo doc = DocumentInfo.fromUri(resolver, uri); if (canCopy(doc, stack.root)) { @@ -271,7 +276,7 @@ class CopyJob extends Job { } } } catch(IOException e) { - failedFileCount += details.getItemCount(); + failedFileCount += srcs.getItemCount(); throw new ResourceException("Failed to open the list of docs to copy.", e); } } @@ -659,7 +664,7 @@ class CopyJob extends Job { .append("CopyJob") .append("{") .append("id=" + id) - .append(", details=" + details) + .append(", docs=" + srcs) .append(", destination=" + stack) .append("}") .toString(); diff --git a/src/com/android/documentsui/services/DeleteJob.java b/src/com/android/documentsui/services/DeleteJob.java index f5bc85e49..f6202c561 100644 --- a/src/com/android/documentsui/services/DeleteJob.java +++ b/src/com/android/documentsui/services/DeleteJob.java @@ -17,6 +17,7 @@ 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; @@ -25,7 +26,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; import com.android.documentsui.Metrics; import com.android.documentsui.R; import com.android.documentsui.model.DocumentInfo; @@ -41,18 +42,18 @@ final class DeleteJob extends Job { private volatile int mDocsProcessed = 0; + Uri mSrcParent; /** * Moves files to a destination identified by {@code destination}. * Performs most work by delegating to CopyJob, then deleting * a file after it has been copied. * * @see @link {@link Job} constructor for most param descriptions. - * - * @param details details that contains files to be deleted and their parent */ - DeleteJob(Context service, Context appContext, Listener listener, - String id, DocumentStack stack, ClipDetails details) { - super(service, appContext, listener, id, stack, details); + DeleteJob(Context service, Listener listener, String id, Uri srcParent, DocumentStack stack, + UrisSupplier srcs) { + super(service, listener, id, OPERATION_DELETE, stack, srcs); + mSrcParent = srcParent; } @Override @@ -71,9 +72,9 @@ final class DeleteJob extends Job { @Override public Notification getProgressNotification() { - mProgressBuilder.setProgress(details.getItemCount(), mDocsProcessed, false); + mProgressBuilder.setProgress(srcs.getItemCount(), mDocsProcessed, false); String format = service.getString(R.string.delete_progress); - mProgressBuilder.setSubText(String.format(format, mDocsProcessed, details.getItemCount())); + mProgressBuilder.setSubText(String.format(format, mDocsProcessed, srcs.getItemCount())); mProgressBuilder.setContentText(null); @@ -94,12 +95,12 @@ final class DeleteJob extends Job { @Override void start() { try { - final List<DocumentInfo> srcs = new ArrayList<>(details.getItemCount()); + final List<DocumentInfo> srcs = new ArrayList<>(this.srcs.getItemCount()); - final Iterable<Uri> uris = details.getDocs(appContext); + final Iterable<Uri> uris = this.srcs.getDocs(appContext); final ContentResolver resolver = appContext.getContentResolver(); - final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, details.getSrcParent()); + final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, mSrcParent); for (Uri uri : uris) { DocumentInfo doc = DocumentInfo.fromUri(resolver, uri); srcs.add(doc); @@ -122,7 +123,7 @@ final class DeleteJob extends Job { 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(); + failedFileCount += srcs.getItemCount(); } } @@ -132,7 +133,8 @@ final class DeleteJob extends Job { .append("DeleteJob") .append("{") .append("id=" + id) - .append(", details=" + details) + .append(", docs=" + srcs) + .append(", srcParent=" + mSrcParent) .append(", location=" + stack) .append("}") .toString(); diff --git a/src/com/android/documentsui/services/FileOperation.java b/src/com/android/documentsui/services/FileOperation.java new file mode 100644 index 000000000..ce63864d3 --- /dev/null +++ b/src/com/android/documentsui/services/FileOperation.java @@ -0,0 +1,240 @@ +/* + * 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.services; + +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.content.Context; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.VisibleForTesting; + +import com.android.documentsui.UrisSupplier; +import com.android.documentsui.model.DocumentStack; +import com.android.documentsui.services.FileOperationService.OpType; + +/** + * FileOperation describes a file operation, such as move/copy/delete etc. + */ +public abstract class FileOperation implements Parcelable { + private final @OpType int mOpType; + + private final UrisSupplier mSrcs; + private DocumentStack mDestination; + + @VisibleForTesting + FileOperation(@OpType int opType, UrisSupplier srcs, DocumentStack destination) { + assert(opType != OPERATION_UNKNOWN); + assert(srcs.getItemCount() > 0); + + mOpType = opType; + mSrcs = srcs; + mDestination = destination; + } + + @Override + public int describeContents() { + return 0; + } + + public @OpType int getOpType() { + return mOpType; + } + + public UrisSupplier getSrc() { + return mSrcs; + } + + public DocumentStack getDestination() { + return mDestination; + } + + public void setDestination(DocumentStack destination) { + mDestination = destination; + } + + public void dispose(Context context) { + mSrcs.dispose(context); + } + + abstract Job createJob(Context service, Job.Listener listener, String id); + + private void appendInfoTo(StringBuilder builder) { + builder.append("opType=").append(mOpType); + builder.append(", srcs=").append(mSrcs.toString()); + builder.append(", destination=").append(mDestination.toString()); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + out.writeInt(mOpType); + out.writeParcelable(mSrcs, flag); + out.writeParcelable(mDestination, flag); + } + + private FileOperation(Parcel in) { + mOpType = in.readInt(); + mSrcs = in.readParcelable(FileOperation.class.getClassLoader()); + mDestination = in.readParcelable(FileOperation.class.getClassLoader()); + } + + public static class CopyOperation extends FileOperation { + private CopyOperation(UrisSupplier srcs, DocumentStack destination) { + super(OPERATION_COPY, srcs, destination); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("CopyOperation{"); + super.appendInfoTo(builder); + builder.append("}"); + + return builder.toString(); + } + + CopyJob createJob(Context service, Job.Listener listener, String id) { + return new CopyJob(service, listener, id, getDestination(), getSrc()); + } + + private CopyOperation(Parcel in) { + super(in); + } + + public static final Parcelable.Creator<CopyOperation> CREATOR = + new Parcelable.Creator<CopyOperation>() { + + @Override + public CopyOperation createFromParcel(Parcel source) { + return new CopyOperation(source); + } + + @Override + public CopyOperation[] newArray(int size) { + return new CopyOperation[size]; + } + }; + } + + public static class MoveDeleteOperation extends FileOperation { + private final Uri mSrcParent; + + private MoveDeleteOperation( + @OpType int opType, UrisSupplier srcs, Uri srcParent, DocumentStack destination) { + super(opType, srcs, destination); + + assert(srcParent != null); + mSrcParent = srcParent; + } + + @Override + Job createJob(Context service, Job.Listener listener, String id) { + switch(getOpType()) { + case OPERATION_MOVE: + return new MoveJob( + service, listener, id, mSrcParent, getDestination(), getSrc()); + case OPERATION_DELETE: + return new DeleteJob( + service, listener, id, mSrcParent, getDestination(), getSrc()); + default: + throw new UnsupportedOperationException("Unsupported op type: " + getOpType()); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("MoveDeleteOperation{"); + super.appendInfoTo(builder); + builder.append(", srcParent=").append(mSrcParent.toString()); + builder.append("}"); + + return builder.toString(); + } + + @Override + public void writeToParcel(Parcel out, int flag) { + super.writeToParcel(out, flag); + out.writeParcelable(mSrcParent, flag); + } + + private MoveDeleteOperation(Parcel in) { + super(in); + mSrcParent = in.readParcelable(null); + } + + public static final Parcelable.Creator<MoveDeleteOperation> CREATOR = + new Parcelable.Creator<MoveDeleteOperation>() { + + + @Override + public MoveDeleteOperation createFromParcel(Parcel source) { + return new MoveDeleteOperation(source); + } + + @Override + public MoveDeleteOperation[] newArray(int size) { + return new MoveDeleteOperation[size]; + } + }; + } + + public static class Builder { + private @OpType int mOpType; + private Uri mSrcParent; + private UrisSupplier mSrcs; + private DocumentStack mDestination; + + public Builder withOpType(@OpType int opType) { + mOpType = opType; + return this; + } + + public Builder withSrcParent(Uri srcParent) { + mSrcParent = srcParent; + return this; + } + + public Builder withSrcs(UrisSupplier srcs) { + mSrcs = srcs; + return this; + } + + public Builder withDestination(DocumentStack destination) { + mDestination = destination; + return this; + } + + public FileOperation build() { + switch (mOpType) { + case OPERATION_COPY: + return new CopyOperation(mSrcs, mDestination); + case OPERATION_MOVE: + case OPERATION_DELETE: + return new MoveDeleteOperation(mOpType, mSrcs, mSrcParent, mDestination); + default: + throw new UnsupportedOperationException("Unsupported op type: " + mOpType); + } + } + } +} diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java index 164d309ec..419bf1395 100644 --- a/src/com/android/documentsui/services/FileOperationService.java +++ b/src/com/android/documentsui/services/FileOperationService.java @@ -25,15 +25,9 @@ import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; -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.DocumentStack; -import com.android.documentsui.services.Job.Factory; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -55,13 +49,17 @@ public class FileOperationService extends Service implements Job.Listener { public static final String TAG = "FileOperationService"; + // Extra used for OperationDialogFragment, Notifications and picking copy destination. + public static final String EXTRA_OPERATION_TYPE = "com.android.documentsui.OPERATION_TYPE"; + + // Extras used for OperationDialogFragment... + public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE"; + public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST"; + + // Extras used to start or cancel a file operation... 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_CLIP_DETAILS = "com.android.documentsui.SRC_CLIP_DETAIL"; - public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE"; - - public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST"; @IntDef(flag = true, value = { OPERATION_UNKNOWN, @@ -86,7 +84,6 @@ public class FileOperationService extends Service implements Job.Listener { // Use a separate thread pool to prioritize deletions. @VisibleForTesting ExecutorService deletionExecutor; - @VisibleForTesting Factory jobFactory; // Use a handler to schedule monitor tasks. @VisibleForTesting Handler handler; @@ -111,10 +108,6 @@ public class FileOperationService extends Service implements Job.Listener { deletionExecutor = Executors.newCachedThreadPool(); } - if (jobFactory == null) { - jobFactory = Job.Factory.instance; - } - if (handler == null) { // Monitor tasks are small enough to schedule them on main thread. handler = new Handler(); @@ -159,9 +152,8 @@ public class FileOperationService extends Service implements Job.Listener { if (intent.hasExtra(EXTRA_CANCEL)) { handleCancel(intent); } else { - ClipDetails details = intent.getParcelableExtra(EXTRA_CLIP_DETAILS); - assert(details.getOpType() != OPERATION_UNKNOWN); - handleOperation(intent, jobId, details); + FileOperation operation = intent.getParcelableExtra(EXTRA_OPERATION); + handleOperation(jobId, operation); } // Track the service supplied id so we can stop the service once we're out of work to do. @@ -170,15 +162,19 @@ public class FileOperationService extends Service implements Job.Listener { return START_NOT_STICKY; } - private void handleOperation(Intent intent, String jobId, ClipDetails details) { + private void handleOperation(String jobId, FileOperation operation) { synchronized (mRunning) { if (mWakeLock == null) { mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); } - DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK); + if (mRunning.containsKey(jobId)) { + Log.w(TAG, "Duplicate job id: " + jobId + + ". Ignoring job request for operation: " + operation + "."); + return; + } - Job job = createJob(jobId, details, stack); + Job job = operation.createJob(this, this, jobId); if (job == null) { return; @@ -188,7 +184,7 @@ public class FileOperationService extends Service implements Job.Listener { assert (job != null); if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + "."); - Future<?> future = getExecutorService(details.getOpType()).submit(job); + Future<?> future = getExecutorService(operation.getOpType()).submit(job); mRunning.put(jobId, new JobRecord(job, future)); } } @@ -226,37 +222,6 @@ public class FileOperationService extends Service implements Job.Listener { // TODO: Guarantee the job is being finalized } - /** - * Creates a new job. Returns null if a job with {@code id} already exists. - * @return - */ - @GuardedBy("mRunning") - private @Nullable Job createJob( - String id, ClipDetails details, DocumentStack stack) { - - assert(details.getItemCount() > 0); - - if (mRunning.containsKey(id)) { - Log.w(TAG, "Duplicate job id: " + id - + ". Ignoring job request for details: " + details + ", stack: " + stack + "."); - return null; - } - - switch (details.getOpType()) { - case OPERATION_COPY: - return jobFactory.createCopy( - this, getApplicationContext(), this, id, stack, details); - case OPERATION_MOVE: - return jobFactory.createMove( - this, getApplicationContext(), this, id, stack, details); - case OPERATION_DELETE: - return jobFactory.createDelete( - this, getApplicationContext(), this, id, stack, details); - default: - throw new UnsupportedOperationException(); - } - } - private ExecutorService getExecutorService(@OpType int operationType) { switch (operationType) { case OPERATION_COPY: diff --git a/src/com/android/documentsui/services/FileOperations.java b/src/com/android/documentsui/services/FileOperations.java index 034c0d7c3..01956a1b0 100644 --- a/src/com/android/documentsui/services/FileOperations.java +++ b/src/com/android/documentsui/services/FileOperations.java @@ -19,21 +19,17 @@ 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.services.FileOperationService.EXTRA_CANCEL; import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID; -import static com.android.documentsui.services.FileOperationService.EXTRA_CLIP_DETAILS; +import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION; import android.annotation.IntDef; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.os.Parcelable; import android.support.annotation.VisibleForTesting; import android.util.Log; -import com.android.documentsui.ClipDetails; -import com.android.documentsui.model.DocumentStack; import com.android.documentsui.services.FileOperationService.OpType; import java.lang.annotation.Retention; @@ -57,16 +53,15 @@ public final class FileOperations { /** * Tries to start the activity. Returns the job id. */ - public static String start(Context context, ClipDetails details, - DocumentStack stack, Callback callback) { + public static String start(Context context, FileOperation operation, Callback callback) { if (DEBUG) Log.d(TAG, "Handling generic 'start' call."); String jobId = createJobId(); - Intent intent = createBaseIntent(context, jobId, details, stack); + Intent intent = createBaseIntent(context, jobId, operation); - callback.onOperationResult( - Callback.STATUS_ACCEPTED, details.getOpType(), details.getItemCount()); + callback.onOperationResult(Callback.STATUS_ACCEPTED, operation.getOpType(), + operation.getSrc().getItemCount()); context.startService(intent); @@ -89,17 +84,14 @@ public final class FileOperations { * * @param jobId A unique jobid for this job. * Use {@link #createJobId} if you don't have one handy. - * @param details the clip details that contains source files and their parent * @return Id of the job. */ public static Intent createBaseIntent( - Context context, String jobId, ClipDetails details, - DocumentStack localeStack) { + Context context, String jobId, FileOperation operation) { Intent intent = new Intent(context, FileOperationService.class); intent.putExtra(EXTRA_JOB_ID, jobId); - intent.putExtra(EXTRA_CLIP_DETAILS, details); - intent.putExtra(EXTRA_STACK, (Parcelable) localeStack); + intent.putExtra(EXTRA_OPERATION, operation); return intent; } diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java index 0b4735f3a..29e02101c 100644 --- a/src/com/android/documentsui/services/Job.java +++ b/src/com/android/documentsui/services/Job.java @@ -20,11 +20,8 @@ import static com.android.documentsui.DocumentsApplication.acquireUnstableProvid import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL; import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE; 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_OPERATION_TYPE; 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; @@ -43,7 +40,7 @@ import android.os.RemoteException; import android.provider.DocumentsContract; import android.util.Log; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; import com.android.documentsui.FilesActivity; import com.android.documentsui.Metrics; import com.android.documentsui.OperationDialogFragment; @@ -91,7 +88,7 @@ abstract public class Job implements Runnable { final @OpType int operationType; final String id; final DocumentStack stack; - final ClipDetails details; + final UrisSupplier srcs; int failedFileCount = 0; final ArrayList<DocumentInfo> failedFiles = new ArrayList<>(); @@ -104,28 +101,26 @@ 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 service The service context in which this job is running. - * @param appContext The context of the invoking application. This is usually - * just {@code getApplicationContext()}. * @param listener * @param id Arbitrary string ID * @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} + * @param srcs the list of docs to operate on */ - Job(Context service, Context appContext, Listener listener, - String id, DocumentStack stack, ClipDetails details) { + Job(Context service, Listener listener, String id, + @OpType int opType, DocumentStack stack, UrisSupplier srcs) { - assert(details.getOpType() != OPERATION_UNKNOWN); + assert(opType != OPERATION_UNKNOWN); this.service = service; - this.appContext = appContext; + this.appContext = service.getApplicationContext(); this.listener = listener; - this.operationType = details.getOpType(); + this.operationType = opType; this.id = id; this.stack = stack; - this.details = details; + this.srcs = srcs; mProgressBuilder = createProgressBuilder(); } @@ -156,7 +151,7 @@ abstract public class Job implements Runnable { // 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); + srcs.dispose(appContext); } } @@ -255,7 +250,7 @@ abstract public class Job implements Runnable { Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) { final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE); navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE); - navigateIntent.putExtra(EXTRA_OPERATION, operationType); + navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType); navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles); final Notification.Builder errorBuilder = new Notification.Builder(service) @@ -330,40 +325,6 @@ abstract public class Job implements Runnable { } /** - * Factory class that facilitates our testing FileOperationService. - */ - static class Factory { - - static final Factory instance = new Factory(); - - Job createCopy(Context service, Context appContext, Listener listener, - 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, details); - } - - Job createMove(Context service, Context appContext, Listener listener, - 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, details); - } - - Job createDelete(Context service, Context appContext, Listener listener, - 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, details); - } - } - - /** * Listener interface employed by the service that owns us as well as tests. */ interface Listener { diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java index 75c4dc065..5e9d5cca0 100644 --- a/src/com/android/documentsui/services/MoveJob.java +++ b/src/com/android/documentsui/services/MoveJob.java @@ -17,17 +17,19 @@ 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.net.Uri; 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.UrisSupplier; import com.android.documentsui.R; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; @@ -39,6 +41,7 @@ final class MoveJob extends CopyJob { private static final String TAG = "MoveJob"; + Uri mSrcParentUri; DocumentInfo mSrcParent; /** @@ -47,12 +50,11 @@ final class MoveJob extends CopyJob { * a file after it has been copied. * * @see @link {@link Job} constructor for most param descriptions. - * - * @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, ClipDetails details) { - super(service, appContext, listener, id, destination, details); + MoveJob(Context service, Listener listener, + String id, Uri srcParent, DocumentStack destination, UrisSupplier srcs) { + super(service, listener, id, OPERATION_MOVE, destination, srcs); + mSrcParentUri = srcParent; } @Override @@ -81,16 +83,21 @@ final class MoveJob extends CopyJob { } @Override - public void start() { + public boolean setUp() { final ContentResolver resolver = appContext.getContentResolver(); try { - mSrcParent = DocumentInfo.fromUri(resolver, details.getSrcParent()); + mSrcParent = DocumentInfo.fromUri(resolver, mSrcParentUri); } catch(FileNotFoundException e) { Log.e(TAG, "Failed to create srcParent.", e); - failedFileCount += details.getItemCount(); - return; + failedFileCount += srcs.getItemCount(); + return false; } + return super.setUp(); + } + + @Override + public void start() { super.start(); } diff --git a/tests/src/com/android/documentsui/ClipDetailsTest.java b/tests/src/com/android/documentsui/UrisSupplierTest.java index b0647b89c..719f0e240 100644 --- a/tests/src/com/android/documentsui/ClipDetailsTest.java +++ b/tests/src/com/android/documentsui/UrisSupplierTest.java @@ -25,8 +25,6 @@ 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; @@ -42,12 +40,9 @@ import java.util.List; @RunWith(AndroidJUnit4.class) @MediumTest -public class ClipDetailsTest { +public class UrisSupplierTest { 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); @@ -71,72 +66,58 @@ public class ClipDetailsTest { } @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(); + UrisSupplier uris = createWithShortList(); - assertEquals(SHORT_URI_LIST.size(), details.getItemCount()); + assertEquals(SHORT_URI_LIST.size(), uris.getItemCount()); } @Test public void testItemCountEquals_longList() { - ClipDetails details = createDetailsWithLongList(); + UrisSupplier uris = createWithLongList(); - assertEquals(LONG_URI_LIST.size(), details.getItemCount()); + assertEquals(LONG_URI_LIST.size(), uris.getItemCount()); } @Test public void testGetDocsEquals_shortList() throws Exception { - ClipDetails details = createDetailsWithShortList(); + UrisSupplier uris = createWithShortList(); - assertIterableEquals(SHORT_URI_LIST, details.getDocs(mStorage)); + assertIterableEquals(SHORT_URI_LIST, uris.getDocs(mStorage)); } @Test public void testGetDocsEquals_longList() throws Exception { - ClipDetails details = createDetailsWithLongList(); + UrisSupplier uris = createWithLongList(); - assertIterableEquals(LONG_URI_LIST, details.getDocs(mStorage)); + assertIterableEquals(LONG_URI_LIST, uris.getDocs(mStorage)); } @Test public void testDispose_shortList() throws Exception { - ClipDetails details = createDetailsWithShortList(); + UrisSupplier uris = createWithShortList(); - details.dispose(mStorage); + uris.dispose(mStorage); } @Test public void testDispose_longList() throws Exception { - ClipDetails details = createDetailsWithLongList(); + UrisSupplier uris = createWithLongList(); - details.dispose(mStorage); + uris.dispose(mStorage); } - private ClipDetails createDetailsWithShortList() { - return ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, SHORT_URI_LIST, mStorage); + private UrisSupplier createWithShortList() { + return UrisSupplier.create(SHORT_URI_LIST, mStorage); } - private ClipDetails createDetailsWithLongList() { - ClipDetails details = - ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, LONG_URI_LIST, mStorage); + private UrisSupplier createWithLongList() { + UrisSupplier uris = + UrisSupplier.create(LONG_URI_LIST, mStorage); mExecutor.runAll(); - return details; + return uris; } private void assertIterableEquals(Iterable<Uri> expected, Iterable<Uri> value) { diff --git a/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java b/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java index cd0593946..2560f2c94 100644 --- a/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java +++ b/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java @@ -16,8 +16,6 @@ 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; @@ -25,12 +23,19 @@ import android.provider.DocumentsContract; import android.test.suitebuilder.annotation.MediumTest; import com.android.documentsui.model.DocumentInfo; +import com.android.documentsui.services.FileOperationService.OpType; import java.util.List; @MediumTest public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJobTest<T> { + private final @OpType int mOpType; + + AbstractCopyJobTest(@OpType int opType) { + mOpType = opType; + } + public void runCopyFilesTest() throws Exception { Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt"); mDocs.writeDocument(testFile1, HAM_BYTES); @@ -111,7 +116,7 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob public void runNoCopyDirToSelfTest() throws Exception { Uri testDir = mDocs.createFolder(mSrcRoot, "someDir"); - createJob(OPERATION_COPY, + createJob(mOpType, newArrayList(testDir), DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId), testDir).run(); @@ -127,7 +132,7 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob Uri testDir = mDocs.createFolder(mSrcRoot, "someDir"); Uri destDir = mDocs.createFolder(testDir, "theDescendent"); - createJob(OPERATION_COPY, + createJob(mOpType, newArrayList(testDir), DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId), destDir).run(); @@ -163,6 +168,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(OPERATION_COPY, srcs, srcParent, destination); + return createJob(mOpType, srcs, srcParent, destination); } } diff --git a/tests/src/com/android/documentsui/services/AbstractJobTest.java b/tests/src/com/android/documentsui/services/AbstractJobTest.java index c3cbe3f14..053942b82 100644 --- a/tests/src/com/android/documentsui/services/AbstractJobTest.java +++ b/tests/src/com/android/documentsui/services/AbstractJobTest.java @@ -27,14 +27,14 @@ import android.os.RemoteException; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; 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.android.documentsui.services.FileOperationService.OpType; -import com.android.documentsui.testing.ClipDetailsFactory; +import com.android.documentsui.testing.DocsProviders; import java.util.List; @@ -91,10 +91,13 @@ public abstract class AbstractJobTest<T extends Job> extends AndroidTestCase { stack.push(DocumentInfo.fromUri(mResolver, destination)); stack.root = mSrcRoot; - ClipDetails details = ClipDetailsFactory.createClipDetails(opType, srcParent, srcs); - return createJob(details, stack); + UrisSupplier urisSupplier = DocsProviders.createDocsProvider(srcs); + FileOperation operation = new FileOperation.Builder() + .withOpType(opType) + .withSrcs(urisSupplier) + .withDestination(stack) + .withSrcParent(srcParent) + .build(); + return (T) operation.createJob(mContext, mJobListener, FileOperations.createJobId()); } - - abstract T createJob(ClipDetails details, DocumentStack destination) - throws Exception; } diff --git a/tests/src/com/android/documentsui/services/CopyJobTest.java b/tests/src/com/android/documentsui/services/CopyJobTest.java index eac06ca98..64211c20e 100644 --- a/tests/src/com/android/documentsui/services/CopyJobTest.java +++ b/tests/src/com/android/documentsui/services/CopyJobTest.java @@ -16,18 +16,21 @@ 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; import android.provider.DocumentsContract.Document; import android.test.suitebuilder.annotation.MediumTest; -import com.android.documentsui.ClipDetails; -import com.android.documentsui.model.DocumentStack; - @MediumTest public class CopyJobTest extends AbstractCopyJobTest<CopyJob> { + public CopyJobTest() { + super(OPERATION_COPY); + } + public void testCopyFiles() throws Exception { runCopyFilesTest(); } @@ -74,11 +77,4 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> { public void testCopyFileWithReadErrors() throws Exception { runCopyFileWithReadErrorsTest(); } - - @Override - CopyJob createJob(ClipDetails details, DocumentStack stack) - throws Exception { - return new CopyJob( - mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details); - } } diff --git a/tests/src/com/android/documentsui/services/DeleteJobTest.java b/tests/src/com/android/documentsui/services/DeleteJobTest.java index 050c7ea59..9dbe7cee5 100644 --- a/tests/src/com/android/documentsui/services/DeleteJobTest.java +++ b/tests/src/com/android/documentsui/services/DeleteJobTest.java @@ -24,9 +24,6 @@ import android.net.Uri; import android.provider.DocumentsContract; import android.test.suitebuilder.annotation.MediumTest; -import com.android.documentsui.ClipDetails; -import com.android.documentsui.model.DocumentStack; - import java.util.List; @MediumTest @@ -53,12 +50,4 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> { Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId); return createJob(OPERATION_DELETE, srcs, srcParent, stack); } - - // TODO: Remove inheritance, as stack is not used for deleting, nor srcParent. - @Override - DeleteJob createJob(ClipDetails details, DocumentStack stack) - throws Exception { - return new DeleteJob( - mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details); - } } diff --git a/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/tests/src/com/android/documentsui/services/FileOperationServiceTest.java index e16d5ae52..b49d15d3c 100644 --- a/tests/src/com/android/documentsui/services/FileOperationServiceTest.java +++ b/tests/src/com/android/documentsui/services/FileOperationServiceTest.java @@ -18,6 +18,7 @@ package com.android.documentsui.services; 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.OpType; import static com.android.documentsui.services.FileOperations.createBaseIntent; import static com.android.documentsui.services.FileOperations.createJobId; @@ -26,14 +27,15 @@ import static com.google.android.collect.Lists.newArrayList; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; import android.test.ServiceTestCase; import android.test.suitebuilder.annotation.MediumTest; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; 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.DocsProviders; import com.android.documentsui.testing.TestHandler; import com.android.documentsui.testing.TestScheduledExecutorService; @@ -50,11 +52,13 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi private static final DocumentInfo GAMMA_DOC = createDoc("gamma"); private static final DocumentInfo DELTA_DOC = createDoc("delta"); + private final List<TestJob> mCopyJobs = new ArrayList<>(); + private final List<TestJob> mDeleteJobs = new ArrayList<>(); + private FileOperationService mService; private TestScheduledExecutorService mExecutor; private TestScheduledExecutorService mDeletionExecutor; private TestHandler mHandler; - private TestJobFactory mJobFactory; public FileOperationServiceTest() { super(FileOperationService.class); @@ -68,7 +72,9 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi mExecutor = new TestScheduledExecutorService(); mDeletionExecutor = new TestScheduledExecutorService(); mHandler = new TestHandler(); - mJobFactory = new TestJobFactory(); + + mCopyJobs.clear(); + mDeleteJobs.clear(); // Install test doubles. mService = getService(); @@ -81,9 +87,13 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi assertNull(mService.handler); mService.handler = mHandler; + } - assertNull(mService.jobFactory); - mService.jobFactory = mJobFactory; + @Override + protected void tearDown() { + // There are lots of progress notifications generated in this test case. + // Dismiss all of them here. + mHandler.dispatchAllMessages(); } public void testRunsCopyJobs() throws Exception { @@ -91,7 +101,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC)); mExecutor.runAll(); - mJobFactory.assertAllCopyJobsStarted(); + assertAllCopyJobsStarted(); } public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception { @@ -102,20 +112,20 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi } startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC)); - mJobFactory.assertJobsCreated(1); + assertJobsCreated(1); mExecutor.runAll(); - mJobFactory.assertAllCopyJobsStarted(); + assertAllCopyJobsStarted(); } public void testRunsCopyJobs_AfterFailure() throws Exception { startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC)); - mJobFactory.copyJobs.get(0).fail(ALPHA_DOC); + mCopyJobs.get(0).fail(ALPHA_DOC); mExecutor.runAll(); - mJobFactory.assertAllCopyJobsStarted(); + assertAllCopyJobsStarted(); } public void testRunsCopyJobs_notRunsDeleteJobs() throws Exception { @@ -123,14 +133,14 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi startService(createDeleteIntent(newArrayList(GAMMA_DOC))); mExecutor.runAll(); - mJobFactory.assertNoDeleteJobsStarted(); + assertNoDeleteJobsStarted(); } public void testRunsDeleteJobs() throws Exception { startService(createDeleteIntent(newArrayList(ALPHA_DOC))); mDeletionExecutor.runAll(); - mJobFactory.assertAllDeleteJobsStarted(); + assertAllDeleteJobsStarted(); } public void testRunsDeleteJobs_NotRunsCopyJobs() throws Exception { @@ -138,7 +148,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi startService(createDeleteIntent(newArrayList(GAMMA_DOC))); mDeletionExecutor.runAll(); - mJobFactory.assertNoCopyJobsStarted(); + assertNoCopyJobsStarted(); } public void testUpdatesNotification() throws Exception { @@ -148,7 +158,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi // Assert monitoring continues until job is done assertTrue(mHandler.hasScheduledMessage()); // Two notifications -- one for setup; one for progress - assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications()); + assertEquals(2, mCopyJobs.get(0).getNumOfNotifications()); } public void testStopsUpdatingNotificationAfterFinished() throws Exception { @@ -160,7 +170,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi assertFalse(mHandler.hasScheduledMessage()); // Assert no more notification is generated after finish. - assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications()); + assertEquals(2, mCopyJobs.get(0).getNumOfNotifications()); } @@ -202,7 +212,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC)); - mJobFactory.copyJobs.get(0).fail(ALPHA_DOC); + mCopyJobs.get(0).fail(ALPHA_DOC); mExecutor.runAll(); shutdownService(); @@ -214,8 +224,8 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC)); - mJobFactory.copyJobs.get(0).fail(ALPHA_DOC); - mJobFactory.copyJobs.get(1).fail(GAMMA_DOC); + mCopyJobs.get(0).fail(ALPHA_DOC); + mCopyJobs.get(1).fail(GAMMA_DOC); mExecutor.runAll(); shutdownService(); @@ -233,10 +243,10 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi uris.add(file.derivedUri); } - ClipDetails details = - ClipDetailsFactory.createClipDetails(OPERATION_COPY, SRC_PARENT, uris); + UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris); + TestFileOperation operation = new TestFileOperation(OPERATION_COPY, urisSupplier, stack); - return createBaseIntent(getContext(), createJobId(), details, stack); + return createBaseIntent(getContext(), createJobId(), operation); } private Intent createDeleteIntent(ArrayList<DocumentInfo> files) { @@ -247,10 +257,10 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi uris.add(file.derivedUri); } - ClipDetails details = - ClipDetailsFactory.createClipDetails(OPERATION_DELETE, SRC_PARENT, uris); + UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris); + TestFileOperation operation = new TestFileOperation(OPERATION_DELETE, urisSupplier, stack); - return createBaseIntent(getContext(), createJobId(), details, stack); + return createBaseIntent(getContext(), createJobId(), operation); } private static DocumentInfo createDoc(String name) { @@ -264,6 +274,33 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi return createDoc(uri); } + void assertAllCopyJobsStarted() { + for (TestJob job : mCopyJobs) { + job.assertStarted(); + } + } + + void assertAllDeleteJobsStarted() { + for (TestJob job : mDeleteJobs) { + job.assertStarted(); + } + } + + void assertNoCopyJobsStarted() { + for (TestJob job : mCopyJobs) { + job.assertNotStarted(); + } + } + + void assertNoDeleteJobsStarted() { + for (TestJob job : mDeleteJobs) { + job.assertNotStarted(); + } + } + + void assertJobsCreated(int expected) { + assertEquals(expected, mCopyJobs.size() + mDeleteJobs.size()); + } private static DocumentInfo createDoc(Uri destination) { DocumentInfo destDoc = new DocumentInfo(); destDoc.derivedUri = destination; @@ -275,72 +312,56 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi mDeletionExecutor.assertShutdown(); } - private final class TestJobFactory extends Job.Factory { + private final class TestFileOperation extends FileOperation { - private final List<TestJob> copyJobs = new ArrayList<>(); - private final List<TestJob> deleteJobs = new ArrayList<>(); - - private Runnable mJobRunnable = () -> { + private final Runnable mJobRunnable = () -> { // The following statement is executed concurrently to Job.start() in real situation. // Call it in TestJob.start() to mimic this behavior. mHandler.dispatchNextMessage(); }; - - void assertAllCopyJobsStarted() { - for (TestJob job : copyJobs) { - job.assertStarted(); - } + private final @OpType int mOpType; + private final UrisSupplier mSrcs; + private final DocumentStack mDestination; + + private TestFileOperation( + @OpType int opType, UrisSupplier srcs, DocumentStack destination) { + super(opType, srcs, destination); + mOpType = opType; + mSrcs = srcs; + mDestination = destination; } - void assertAllDeleteJobsStarted() { - for (TestJob job : deleteJobs) { - job.assertStarted(); - } - } - - void assertNoCopyJobsStarted() { - for (TestJob job : copyJobs) { - job.assertNotStarted(); - } - } + @Override + public Job createJob(Context service, Job.Listener listener, String id) { + TestJob job = + new TestJob(service, listener, id, mOpType, mDestination, mSrcs, mJobRunnable); - void assertNoDeleteJobsStarted() { - for (TestJob job : deleteJobs) { - job.assertNotStarted(); + if (mOpType == OPERATION_COPY) { + mCopyJobs.add(job); } - } - - void assertJobsCreated(int expected) { - assertEquals(expected, copyJobs.size() + deleteJobs.size()); - } - - @Override - Job createCopy(Context service, Context appContext, Listener listener, String id, - DocumentStack stack, ClipDetails details) { - if (details.getItemCount() == 0) { - throw new RuntimeException("Empty srcs not supported!"); + if (mOpType == OPERATION_DELETE) { + mDeleteJobs.add(job); } - TestJob job = new TestJob( - 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, ClipDetails details) { + /** + * CREATOR is required for Parcelables, but we never pass this class via parcel. + */ + public Parcelable.Creator<TestFileOperation> CREATOR = + new Parcelable.Creator<TestFileOperation>() { - if (details.getItemCount() == 0) { - throw new RuntimeException("Empty srcs not supported!"); + @Override + public TestFileOperation createFromParcel(Parcel source) { + throw new UnsupportedOperationException("Can't create from a parcel."); } - TestJob job = new TestJob( - service, appContext, listener, id, stack, details, mJobRunnable); - deleteJobs.add(job); - - return job; - } + @Override + public TestFileOperation[] newArray(int size) { + throw new UnsupportedOperationException("Can't create a new array."); + } + }; } } diff --git a/tests/src/com/android/documentsui/services/MoveJobTest.java b/tests/src/com/android/documentsui/services/MoveJobTest.java index fd5c92a0f..56d96ccae 100644 --- a/tests/src/com/android/documentsui/services/MoveJobTest.java +++ b/tests/src/com/android/documentsui/services/MoveJobTest.java @@ -16,18 +16,21 @@ package com.android.documentsui.services; +import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE; + import static com.google.common.collect.Lists.newArrayList; import android.net.Uri; import android.provider.DocumentsContract.Document; import android.test.suitebuilder.annotation.MediumTest; -import com.android.documentsui.ClipDetails; -import com.android.documentsui.model.DocumentStack; - @MediumTest public class MoveJobTest extends AbstractCopyJobTest<MoveJob> { + public MoveJobTest() { + super(OPERATION_MOVE); + } + public void testMoveFiles() throws Exception { runCopyFilesTest(); @@ -105,11 +108,4 @@ public class MoveJobTest extends AbstractCopyJobTest<MoveJob> { } // TODO: Add test cases for moving when multi-parented. - - @Override - MoveJob createJob(ClipDetails details, DocumentStack stack) - throws Exception { - return new MoveJob( - mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details); - } } diff --git a/tests/src/com/android/documentsui/services/TestJob.java b/tests/src/com/android/documentsui/services/TestJob.java index a7e1d665d..0c273c0b1 100644 --- a/tests/src/com/android/documentsui/services/TestJob.java +++ b/tests/src/com/android/documentsui/services/TestJob.java @@ -23,10 +23,11 @@ import android.app.Notification; import android.app.Notification.Builder; import android.content.Context; -import com.android.documentsui.ClipDetails; +import com.android.documentsui.UrisSupplier; 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 java.text.NumberFormat; @@ -38,9 +39,9 @@ public class TestJob extends Job { private int mNumOfNotifications = 0; TestJob( - Context service, Context appContext, Listener listener, - String id, DocumentStack stack, ClipDetails details, Runnable startRunnable) { - super(service, appContext, listener, id, stack, details); + Context service, Listener listener, String id, + @OpType int opType, DocumentStack stack, UrisSupplier srcs, Runnable startRunnable) { + super(service, listener, id, opType, stack, srcs); mStartRunnable = startRunnable; } diff --git a/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java b/tests/src/com/android/documentsui/testing/DocsProviders.java index d8335281d..d438892f7 100644 --- a/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java +++ b/tests/src/com/android/documentsui/testing/DocsProviders.java @@ -18,15 +18,14 @@ package com.android.documentsui.testing; import android.net.Uri; -import com.android.documentsui.ClipDetails; -import com.android.documentsui.services.FileOperationService.OpType; +import com.android.documentsui.UrisSupplier; import java.util.List; -public final class ClipDetailsFactory { - private ClipDetailsFactory() {} +public final class DocsProviders { + private DocsProviders() {} - public static ClipDetails createClipDetails(@OpType int opType, Uri srcParent, List<Uri> docs) { - return new ClipDetails.StandardClipDetails(opType, srcParent, docs); + public static UrisSupplier createDocsProvider(List<Uri> docs) { + return new UrisSupplier.StandardUrisSupplier(docs); } } diff --git a/tests/src/com/android/documentsui/testing/TestHandler.java b/tests/src/com/android/documentsui/testing/TestHandler.java index c18ef1f90..143ec71b3 100644 --- a/tests/src/com/android/documentsui/testing/TestHandler.java +++ b/tests/src/com/android/documentsui/testing/TestHandler.java @@ -41,6 +41,12 @@ public class TestHandler extends Handler { mTimer.fastForwardToNextTask(); } + public void dispatchAllMessages() { + while (hasScheduledMessage()) { + dispatchNextMessage(); + } + } + @Override public boolean sendMessageAtTime(Message msg, long uptimeMillis) { msg.setTarget(this); |