Merge "Support for concurrent file operations."

This commit is contained in:
Steve McKay
2016-01-20 20:05:02 +00:00
committed by Android (Google) Code Review
15 changed files with 1573 additions and 754 deletions

View File

@@ -17,6 +17,10 @@
package com.android.documentsui.services;
import static android.os.SystemClock.elapsedRealtime;
import static android.provider.DocumentsContract.buildChildDocumentsUri;
import static android.provider.DocumentsContract.buildDocumentUri;
import static android.provider.DocumentsContract.getDocumentId;
import static android.provider.DocumentsContract.isChildDocument;
import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
@@ -44,6 +48,7 @@ import android.webkit.MimeTypeMap;
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 libcore.io.IoUtils;
@@ -80,9 +85,22 @@ class CopyJob extends Job {
*
* @param srcs List of files to be copied.
*/
CopyJob(Context serviceContext, Context appContext, Listener listener,
CopyJob(Context service, Context appContext, Listener listener,
String id, DocumentStack destination, List<DocumentInfo> srcs) {
super(OPERATION_COPY, serviceContext, appContext, listener, id, destination);
super(service, appContext, listener, OPERATION_COPY, id, destination);
checkArgument(!srcs.isEmpty());
this.mSrcFiles = srcs;
}
/**
* @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);
checkArgument(!srcs.isEmpty());
this.mSrcFiles = srcs;
@@ -91,15 +109,15 @@ class CopyJob extends Job {
@Override
Builder createProgressBuilder() {
return super.createProgressBuilder(
serviceContext.getString(R.string.copy_notification_title),
service.getString(R.string.copy_notification_title),
R.drawable.ic_menu_copy,
serviceContext.getString(android.R.string.cancel),
service.getString(android.R.string.cancel),
R.drawable.ic_cab_cancel);
}
@Override
public Notification getSetupNotification() {
return getSetupNotification(serviceContext.getString(R.string.copy_preparing));
return getSetupNotification(service.getString(R.string.copy_preparing));
}
public boolean shouldUpdateProgress() {
@@ -113,7 +131,7 @@ class CopyJob extends Job {
mProgressBuilder.setContentInfo(
NumberFormat.getPercentInstance().format(completed));
if (mRemainingTime > 0) {
mProgressBuilder.setContentText(serviceContext.getString(msgId,
mProgressBuilder.setContentText(service.getString(msgId,
DateUtils.formatDuration(mRemainingTime)));
} else {
mProgressBuilder.setContentText(null);
@@ -164,7 +182,7 @@ class CopyJob extends Job {
}
@Override
void run(FileOperationService service) throws RemoteException {
void start() throws RemoteException {
mStartTime = elapsedRealtime();
// Acquire content providers.
@@ -186,16 +204,14 @@ class CopyJob extends Job {
// Guard unsupported recursive operation.
if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
if (DEBUG) Log.d(TAG, "Skipping recursive operation on directory "
+ dstInfo.derivedUri);
onFileFailed(srcInfo);
onFileFailed(srcInfo,
"Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
continue;
}
if (DEBUG) Log.d(TAG,
"Performing op-type:" + type() + " of " + srcInfo.displayName
+ " (" + srcInfo.derivedUri + ")" + " to " + dstInfo.displayName
+ " (" + dstInfo.derivedUri + ")");
"Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
+ " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
processDocument(srcInfo, dstInfo);
}
@@ -219,7 +235,6 @@ class CopyJob extends Job {
*
* @param srcInfo DocumentInfos for the documents to copy.
* @param dstDirInfo The destination directory.
* @param mode The transfer mode (copy or move).
* @return True on success, false on failure.
* @throws RemoteException
*/
@@ -234,7 +249,8 @@ class CopyJob extends Job {
if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
dstDirInfo.derivedUri) == null) {
onFileFailed(srcInfo);
onFileFailed(srcInfo,
"Provider side copy failed for documents: " + srcInfo.derivedUri + ".");
}
return false;
}
@@ -249,6 +265,7 @@ class CopyJob extends Job {
final String dstMimeType;
final String dstDisplayName;
if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + srcInfo);
// If the file is virtual, but can be converted to another format, then try to copy it
// as such format. Also, append an extension for the target mime type (if known).
if (srcInfo.isVirtualDocument()) {
@@ -261,9 +278,7 @@ class CopyJob extends Job {
dstDisplayName = srcInfo.displayName +
(extension != null ? "." + extension : srcInfo.displayName);
} else {
// The virtual file is not available as any alternative streamable format.
// TODO: Log failures.
onFileFailed(srcInfo);
onFileFailed(srcInfo, "Cannot copy virtual file. No streamable formats available.");
return false;
}
} else {
@@ -277,7 +292,9 @@ class CopyJob extends Job {
dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
if (dstUri == null) {
// If this is a directory, the entire subdir will not be copied over.
onFileFailed(srcInfo);
onFileFailed(srcInfo,
"Couldn't create destination document " + dstDisplayName
+ " in directory " + dstDirInfo.displayName + ".");
return false;
}
@@ -285,7 +302,8 @@ class CopyJob extends Job {
try {
dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
} catch (FileNotFoundException e) {
onFileFailed(srcInfo);
onFileFailed(srcInfo,
"Could not load DocumentInfo for newly created file: " + dstUri + ".");
return false;
}
@@ -327,7 +345,7 @@ class CopyJob extends Job {
srcDirInfo.documentId);
cursor = srcClient.query(queryUri, queryColumns, null, null, null);
DocumentInfo srcInfo;
while (cursor.moveToNext()) {
while (cursor.moveToNext() && !isCanceled()) {
srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
success &= processDocument(srcInfo, dstDirInfo);
}
@@ -374,7 +392,7 @@ class CopyJob extends Job {
dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
byte[] buffer = new byte[8192];
byte[] buffer = new byte[32 * 1024];
int len;
while ((len = src.read(buffer)) != -1) {
if (isCanceled()) {
@@ -389,7 +407,8 @@ class CopyJob extends Job {
srcFile.checkError();
} catch (IOException e) {
success = false;
onFileFailed(srcInfo);
onFileFailed(srcInfo, "Exception thrown while copying from "
+ srcInfo.derivedUri + " to " + dstInfo.derivedUri + ".");
if (dstFile != null) {
try {
@@ -405,7 +424,7 @@ class CopyJob extends Job {
}
if (!success) {
// Clean up half-copied files.
if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
canceller.cancel();
try {
DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
@@ -452,8 +471,7 @@ class CopyJob extends Job {
private static long calculateFileSizesRecursively(
ContentProviderClient client, Uri uri) throws RemoteException {
final String authority = uri.getAuthority();
final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
DocumentsContract.getDocumentId(uri));
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
final String queryColumns[] = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
@@ -468,7 +486,7 @@ class CopyJob extends Job {
if (Document.MIME_TYPE_DIR.equals(
getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
// Recurse into directories.
final Uri dirUri = DocumentsContract.buildDocumentUri(authority,
final Uri dirUri = buildDocumentUri(authority,
getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
result += calculateFileSizesRecursively(client, dirUri);
} else {
@@ -497,9 +515,13 @@ class CopyJob extends Job {
boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
throws RemoteException {
if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
return DocumentsContract.isChildDocument(
dstClient, doc.derivedUri, parentDoc.derivedUri);
return isChildDocument(dstClient, doc.derivedUri, parentDoc.derivedUri);
}
return false;
}
}
private void onFileFailed(DocumentInfo file, String msg) {
Log.w(TAG, msg);
onFileFailed(file);
}
}

View File

@@ -16,36 +16,47 @@
package com.android.documentsui.services;
import static android.os.SystemClock.elapsedRealtime;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import android.annotation.IntDef;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
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.Shared;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.google.common.base.Objects;
import com.android.documentsui.services.Job.Factory;
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;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.concurrent.GuardedBy;
public class FileOperationService extends Service implements Job.Listener {
private static final int DEFAULT_DELAY = 0;
private static final int MAX_DELAY = 10 * 1000; // ten seconds
public class FileOperationService extends IntentService implements Job.Listener {
public static final String TAG = "FileOperationService";
private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
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";
@@ -68,23 +79,31 @@ public class FileOperationService extends IntentService implements Job.Listener
// TODO: Move it to a shared file when more operations are implemented.
public static final int FAILURE_COPY = 1;
private PowerManager mPowerManager;
// The executor and job factory are visible for testing and non-final
// so we'll have a way to inject test doubles from the test. It's
// a sub-optimal arrangement.
@VisibleForTesting ScheduledExecutorService executor;
@VisibleForTesting Factory jobFactory;
private PowerManager mPowerManager;
private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
private NotificationManager mNotificationManager;
// TODO: Rework service to support multiple concurrent jobs.
private volatile Job mJob;
@GuardedBy("mRunning")
private Map<String, JobRecord> mRunning = new HashMap<>();
// For testing only.
@Nullable private TestOnlyListener mJobFinishedListener;
public FileOperationService() {
super("FileOperationService");
}
private int mLastStarted;
@Override
public void onCreate() {
super.onCreate();
// Allow tests to pre-set these with test doubles.
if (executor == null) {
executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
}
if (jobFactory == null) {
jobFactory = Job.Factory.instance;
}
if (DEBUG) Log.d(TAG, "Created.");
mPowerManager = getSystemService(PowerManager.class);
@@ -92,69 +111,50 @@ public class FileOperationService extends IntentService implements Job.Listener
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand: " + intent);
if (intent.hasExtra(EXTRA_CANCEL)) {
handleCancel(intent);
return START_REDELIVER_INTENT;
} else {
return super.onStartCommand(intent, flags, startId);
}
}
@Override
protected void onHandleIntent(Intent intent) {
if (DEBUG) Log.d(TAG, "onHandleIntent: " + intent);
public int onStartCommand(Intent intent, int flags, int startTime) {
// TODO: Ensure we're not being called with retry or redeliver.
// 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);
checkArgument(jobId != null);
if (intent.hasExtra(EXTRA_CANCEL)) {
handleCancel(intent);
return;
} else {
checkArgument(operationType != OPERATION_UNKNOWN);
handleOperation(intent, startTime, jobId, operationType);
}
checkArgument(operationType != OPERATION_UNKNOWN);
return START_NOT_STICKY;
}
PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG);
private void handleOperation(Intent intent, int startTime, String jobId, int operationType) {
if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with start time " + startTime);
ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
// Track start time so we can stop the service once we're out of work to do.
mLastStarted = startTime;
Job job = createJob(operationType, jobId, srcs, stack);
try {
wakeLock.acquire();
mNotificationManager.notify(job.id, 0, job.getSetupNotification());
job.run(this);
} catch (Exception e) {
// Catch-all to prevent any copy errors from wedging the app.
Log.e(TAG, "Exceptions occurred during copying", e);
} finally {
if (DEBUG) Log.d(TAG, "Cleaning up after copy");
job.cleanup();
wakeLock.release();
// Dismiss the ongoing copy notification when the copy is done.
mNotificationManager.cancel(job.id, 0);
if (job.failed()) {
Log.e(TAG, job.failedFiles.size() + " files failed to copy");
mNotificationManager.notify(job.id, 0, job.getFailureNotification());
Job job = null;
synchronized (mRunning) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
// TEST ONLY CODE...<raised eyebrows>
if (mJobFinishedListener != null) {
mJobFinishedListener.onFinished(job.failedFiles);
}
List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
deleteJob(job);
if (DEBUG) Log.d(TAG, "Done cleaning up");
job = createJob(operationType, jobId, srcs, stack);
mWakeLock.acquire();
}
checkState(job != null);
int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
checkArgument(delay <= MAX_DELAY);
ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
mRunning.put(jobId, new JobRecord(job, future));
}
/**
@@ -166,12 +166,25 @@ public class FileOperationService extends IntentService implements Job.Listener
checkArgument(intent.hasExtra(EXTRA_CANCEL));
String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));
// Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
// cancellation requests from affecting unrelated copy jobs. However, if the current job ID
// is null, the service most likely crashed and was revived by the incoming cancel intent.
// In that case, always allow the cancellation to proceed.
if (mJob != null && Objects.equal(jobId, mJob.id)) {
mJob.cancel();
if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
synchronized (mRunning) {
// Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
// cancellation requests from affecting unrelated copy jobs. However, if the current job ID
// is null, the service most likely crashed and was revived by the incoming cancel intent.
// In that case, always allow the cancellation to proceed.
JobRecord record = mRunning.get(jobId);
if (record != null) {
record.job.cancel();
// If the job hasn't been started, cancel it and explicitly clean up.
// If it *has* been started, we wait for it to recognize this, then
// allow it stop working in an orderly fashion.
if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
record.future.cancel(false);
onFinished(record.job);
}
}
}
// Dismiss the progress notification here rather than in the copy loop. This preserves
@@ -179,24 +192,23 @@ public class FileOperationService extends IntentService implements Job.Listener
// Try to cancel it even if we don't have a job id...in case there is some sad
// orphan notification.
mNotificationManager.cancel(jobId, 0);
// TODO: Guarantee the job is being finalized
}
public static String createJobId() {
return String.valueOf(elapsedRealtime());
}
@GuardedBy("mRunning")
private Job createJob(
@OpType int operationType, String id, List<DocumentInfo> srcs, DocumentStack stack) {
Job createJob(
@OpType int operationType, String id, ArrayList<DocumentInfo> srcs,
DocumentStack stack) {
checkState(mJob == null);
checkArgument(!mRunning.containsKey(id));
Job job = null;
switch (operationType) {
case OPERATION_COPY:
mJob = new CopyJob(this, getApplicationContext(), this, id, stack, srcs);
job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
break;
case OPERATION_MOVE:
mJob = new MoveJob(this, getApplicationContext(), this, id, stack, srcs);
job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs);
break;
case OPERATION_DELETE:
throw new UnsupportedOperationException();
@@ -204,42 +216,90 @@ public class FileOperationService extends IntentService implements Job.Listener
throw new UnsupportedOperationException();
}
return checkNotNull(mJob);
return checkNotNull(job);
}
void deleteJob(Job job) {
checkArgument(job == mJob);
mJob = null;
@GuardedBy("mRunning")
private void deleteJob(Job job) {
if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
JobRecord record = mRunning.remove(job.id);
checkArgument(record != null);
record.job.cleanup();
if (mRunning.isEmpty()) {
shutdown();
}
}
/**
* Most likely shuts down. Won't shut down if service has a pending
* message.
*/
private void shutdown() {
if (DEBUG) Log.d(TAG, "Shutting down. Last start time: " + mLastStarted);
mWakeLock.release();
mWakeLock = null;
boolean gonnaStop = stopSelfResult(mLastStarted);
if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
if (!gonnaStop) {
Log.w(TAG, "Service should be stopping, but reports otherwise.");
}
// Sadly "gonnaStop" is always false in tests, so we can't guard executor shutdown.
List<Runnable> unfinished = executor.shutdownNow();
checkState(unfinished.isEmpty());
}
@VisibleForTesting
boolean holdsWakeLock() {
return mWakeLock != null && mWakeLock.isHeld();
}
@Override
public void onStart(Job job) {
if (DEBUG) Log.d(TAG, "onStart: " + job.id);
mNotificationManager.notify(job.id, 0, job.getSetupNotification());
}
@Override
public void onFinished(Job job) {
if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
// Dismiss the ongoing copy notification when the copy is done.
mNotificationManager.cancel(job.id, 0);
synchronized (mRunning) {
deleteJob(job);
}
}
@Override
public void onProgress(CopyJob job) {
if (DEBUG) Log.d(TAG, "On copy progress...");
if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
mNotificationManager.notify(job.id, 0, job.getProgressNotification());
}
@Override
public void onProgress(MoveJob job) {
if (DEBUG) Log.d(TAG, "On move progress...");
mNotificationManager.notify(job.id, 0, job.getProgressNotification());
public void onFailed(Job job) {
if (DEBUG) Log.d(TAG, "onFailed: " + job.id);
checkArgument(job.failed());
Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
mNotificationManager.notify(job.id, 0, job.getFailureNotification());
onFinished(job); // failed jobs don't call finished, so we do.
}
/**
* Sets a callback to be run when the next run job is finished.
* This is test ONLY instrumentation. The alternative is for us to add
* broadcast intents SOLELY for the purpose of testing.
* @param listener
*/
@VisibleForTesting
void addFinishedListener(TestOnlyListener listener) {
this.mJobFinishedListener = listener;
private static final class JobRecord {
private final Job job;
private final ScheduledFuture<?> future;
public JobRecord(Job job, ScheduledFuture<?> future) {
this.job = job;
this.future = future;
}
}
/**
* Only used for testing. Is that obvious enough?
*/
@VisibleForTesting
interface TestOnlyListener {
void onFinished(List<DocumentInfo> failed);
@Override
public IBinder onBind(Intent intent) {
return null; // Boilerplate. See super#onBind
}
}

View File

@@ -16,6 +16,7 @@
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;
@@ -29,6 +30,7 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_DE
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Parcelable;
@@ -52,6 +54,10 @@ public final class FileOperations {
private FileOperations() {}
public static String createJobId() {
return String.valueOf(elapsedRealtime());
}
/**
* Tries to start the activity. Returns the job id.
*/
@@ -78,7 +84,7 @@ public final class FileOperations {
*
* @param context Context for the intent.
* @param jobId The id of the job to cancel.
* Use {@link FileOperationService#createJobId} if you don't have one handy.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @param dstStack The copy destination stack.
*/
@@ -97,13 +103,13 @@ public final class FileOperations {
*
* @param context Context for the intent.
* @param jobId A unique jobid for this job.
* Use {@link FileOperationService#createJobId} if you don't have one handy.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @param destination The copy destination stack.
*/
public static String copy(
Activity activity, List<DocumentInfo> srcDocs, DocumentStack destination) {
String jobId = FileOperationService.createJobId();
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'copy' operation id: " + jobId);
Intent intent = createBaseIntent(OPERATION_COPY, activity, jobId, srcDocs, destination);
@@ -120,13 +126,13 @@ public final class FileOperations {
* Starts the service for a move operation.
*
* @param jobId A unique jobid for this job.
* Use {@link FileOperationService#createJobId} if you don't have one handy.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @param destination The move destination stack.
*/
public static String move(
Activity activity, List<DocumentInfo> srcDocs, DocumentStack destination) {
String jobId = FileOperationService.createJobId();
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'move' operation id: " + jobId);
Intent intent = createBaseIntent(OPERATION_MOVE, activity, jobId, srcDocs, destination);
@@ -143,13 +149,13 @@ public final class FileOperations {
* Starts the service for a move operation.
*
* @param jobId A unique jobid for this job.
* Use {@link FileOperationService#createJobId} if you don't have one handy.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @return Id of the job.
*/
public static String delete(
Activity activity, List<DocumentInfo> srcDocs, DocumentStack location) {
String jobId = FileOperationService.createJobId();
String jobId = createJobId();
if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id: " + jobId);
Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, location);
@@ -162,15 +168,15 @@ public final class FileOperations {
* Starts the service for a move operation.
*
* @param jobId A unique jobid for this job.
* Use {@link FileOperationService#createJobId} if you don't have one handy.
* Use {@link #createJobId} if you don't have one handy.
* @param srcDocs A list of src files to copy.
* @return Id of the job.
*/
public static Intent createBaseIntent(
@OpType int operationType, Activity activity, String jobId,
@OpType int operationType, Context context, String jobId,
List<DocumentInfo> srcDocs, DocumentStack localeStack) {
Intent intent = new Intent(activity, FileOperationService.class);
Intent intent = new Intent(context, FileOperationService.class);
intent.putExtra(EXTRA_JOB_ID, jobId);
intent.putParcelableArrayListExtra(
EXTRA_SRC_LIST, asArrayList(srcDocs));

View File

@@ -16,6 +16,12 @@
package com.android.documentsui.services;
import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
import static com.android.documentsui.services.FileOperationService.EXTRA_FAILURE;
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.FAILURE_COPY;
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -39,14 +45,19 @@ import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;
import java.util.ArrayList;
import java.util.List;
abstract class Job {
/**
* A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
* to do work and show progress relating to this work.
*/
abstract class Job implements Runnable {
final Context serviceContext;
final Context service;
final Context appContext;
final Listener listener;
final @OpType int mOpType;
final @OpType int operationType;
final String id;
final DocumentStack stack;
@@ -58,10 +69,9 @@ abstract class Job {
/**
* A simple progressable job, much like an AsyncTask, but with support
* for providing various related notification, progress and navigation information.
* @param opType
* @param operationType
*
* @param serviceContext The context of the service in which this job is running.
* This is usually just "this".
* @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
@@ -70,14 +80,15 @@ abstract class Job {
* destination in the Files app where the user will be take when the
* navigation intent is invoked (presumably from notification).
*/
Job(@OpType int opType, Context serviceContext, Context appContext, Listener listener,
String id, DocumentStack stack) {
Job(Context service, Context appContext, Listener listener,
@OpType int operationType, String id, DocumentStack stack) {
checkArgument(opType != OPERATION_UNKNOWN);
this.serviceContext = serviceContext;
checkArgument(operationType != OPERATION_UNKNOWN);
this.service = service;
this.appContext = appContext;
this.listener = listener;
mOpType = opType;
this.operationType = operationType;
this.id = id;
this.stack = stack;
@@ -85,13 +96,31 @@ abstract class Job {
mProgressBuilder = createProgressBuilder();
}
abstract void run(FileOperationService service) throws RemoteException;
abstract void cleanup();
@OpType int type() {
return mOpType;
@Override
public final void run() {
listener.onStart(this);
try {
start();
} catch (Exception e) {
// In the case of an unmanaged failure, we still want
// to resolve business in an orderly fashion. That'll
// ensure the service is shut down and notifications
// shown/closed.
listener.onFailed(this);
} finally {
if (failed()) {
listener.onFailed(this);
} else {
listener.onFinished(this);
}
}
}
abstract void start() throws RemoteException;
// Service will call this when it is done with the job.
abstract void cleanup();
abstract Notification getSetupNotification();
// TODO: Progress notification for deletes.
// abstract Notification getProgressNotification(long bytesCopied);
@@ -106,7 +135,7 @@ abstract class Job {
}
final ContentResolver getContentResolver() {
return serviceContext.getContentResolver();
return service.getContentResolver();
}
void onFileFailed(DocumentInfo file) {
@@ -125,15 +154,15 @@ abstract class Job {
Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
final Intent navigateIntent = buildNavigateIntent();
navigateIntent.putExtra(FileOperationService.EXTRA_FAILURE, FileOperationService.FAILURE_COPY);
navigateIntent.putExtra(FileOperationService.EXTRA_OPERATION, mOpType);
navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
navigateIntent.putExtra(EXTRA_OPERATION, operationType);
navigateIntent.putParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST, failedFiles);
navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
final Notification.Builder errorBuilder = new Notification.Builder(serviceContext)
.setContentTitle(serviceContext.getResources().getQuantityString(titleId,
final Notification.Builder errorBuilder = new Notification.Builder(service)
.setContentTitle(service.getResources().getQuantityString(titleId,
failedFiles.size(), failedFiles.size()))
.setContentText(serviceContext.getString(R.string.notification_touch_for_details))
.setContentText(service.getString(R.string.notification_touch_for_details))
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
.setCategory(Notification.CATEGORY_ERROR)
@@ -147,7 +176,7 @@ abstract class Job {
final Builder createProgressBuilder(
String title, @DrawableRes int icon,
String actionTitle, @DrawableRes int actionIcon) {
Notification.Builder progressBuilder = new Notification.Builder(serviceContext)
Notification.Builder progressBuilder = new Notification.Builder(service)
.setContentTitle(title)
.setContentIntent(
PendingIntent.getActivity(appContext, 0, buildNavigateIntent(), 0))
@@ -161,7 +190,7 @@ abstract class Job {
actionIcon,
actionTitle,
PendingIntent.getService(
serviceContext,
service,
0,
cancelIntent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
@@ -173,21 +202,44 @@ abstract class Job {
* Creates an intent for navigating back to the destination directory.
*/
Intent buildNavigateIntent() {
Intent intent = new Intent(serviceContext, FilesActivity.class);
Intent intent = new Intent(service, FilesActivity.class);
intent.setAction(DocumentsContract.ACTION_BROWSE);
intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
return intent;
}
Intent createCancelIntent() {
final Intent cancelIntent = new Intent(serviceContext, FileOperationService.class);
cancelIntent.putExtra(FileOperationService.EXTRA_CANCEL, true);
cancelIntent.putExtra(FileOperationService.EXTRA_JOB_ID, id);
final Intent cancelIntent = new Intent(service, FileOperationService.class);
cancelIntent.putExtra(EXTRA_CANCEL, true);
cancelIntent.putExtra(EXTRA_JOB_ID, id);
return cancelIntent;
}
/**
* 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, List<DocumentInfo> srcs) {
return new CopyJob(service, appContext, listener, id, stack, srcs);
}
Job createMove(Context service, Context appContext, Listener listener,
String id, DocumentStack stack, List<DocumentInfo> srcs) {
return new MoveJob(service, appContext, listener, id, stack, srcs);
}
}
/**
* Listener interface employed by the service that owns us as well as tests.
*/
interface Listener {
void onStart(Job job);
void onFailed(Job job);
void onFinished(Job job);
void onProgress(CopyJob job);
void onProgress(MoveJob job);
}
}

View File

@@ -16,6 +16,8 @@
package com.android.documentsui.services;
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
@@ -43,28 +45,23 @@ final class MoveJob extends CopyJob {
*
* @param srcs List of files to be moved.
*/
MoveJob(Context serviceContext, Context appContext, Listener listener,
MoveJob(Context service, Context appContext, Listener listener,
String id, DocumentStack destination, List<DocumentInfo> srcs) {
super(serviceContext, appContext, listener, id, destination, srcs);
}
@Override
int type() {
return FileOperationService.OPERATION_MOVE;
super(service, appContext, listener, OPERATION_MOVE, id, destination, srcs);
}
@Override
Builder createProgressBuilder() {
return super.createProgressBuilder(
serviceContext.getString(R.string.move_notification_title),
service.getString(R.string.move_notification_title),
R.drawable.ic_menu_copy,
serviceContext.getString(android.R.string.cancel),
service.getString(android.R.string.cancel),
R.drawable.ic_cab_cancel);
}
@Override
public Notification getSetupNotification() {
return getSetupNotification(serviceContext.getString(R.string.move_preparing));
return getSetupNotification(service.getString(R.string.move_preparing));
}
@Override
@@ -78,15 +75,6 @@ final class MoveJob extends CopyJob {
R.plurals.move_error_notification_title, R.drawable.ic_menu_copy);
}
/**
* Copies a the given document to the given location.
*
* @param srcInfo DocumentInfos for the documents to copy.
* @param dstDirInfo The destination directory.
* @param mode The transfer mode (copy or move).
* @return True on success, false on failure.
* @throws RemoteException
*/
@Override
boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
@@ -105,20 +93,22 @@ final class MoveJob extends CopyJob {
}
// If we couldn't do an optimized copy...we fall back to vanilla byte copy.
boolean success = byteCopyDocument(srcInfo, dstDirInfo);
boolean copied = byteCopyDocument(srcInfo, dstDirInfo);
if (success) {
// This is racey. We should make sure that we never delete a directory after
// it changed, so we don't remove a file which had not been copied earlier
// to the target location.
try {
DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
} catch (RemoteException e) {
Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
return false;
}
return copied && !isCanceled() && deleteSrcDocument(srcInfo);
}
private boolean deleteSrcDocument(DocumentInfo srcInfo) {
// This is racey. We should make sure that we never delete a directory after
// it changed, so we don't remove a file which had not been copied earlier
// to the target location.
try {
DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
} catch (RemoteException e) {
Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
return false;
}
return success;
return true; // victory dance!
}
}

View File

@@ -16,47 +16,68 @@
package com.android.documentsui;
import static android.provider.DocumentsContract.buildChildDocumentsUri;
import static android.provider.DocumentsContract.buildDocumentUri;
import static android.provider.DocumentsContract.buildRootsUri;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.fail;
import android.content.ContentProviderClient;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.support.annotation.Nullable;
import android.test.MoreAsserts;
import android.util.Log;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.google.android.collect.Lists;
import libcore.io.IoUtils;
import libcore.io.Streams;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Provides support for creation of documents in a test settings.
*/
public class DocumentsProviderHelper {
private final ContentProviderClient mClient;
private final String mAuthority;
private final ContentProviderClient mClient;
public DocumentsProviderHelper(String authority, ContentProviderClient client) {
mClient = client;
mAuthority = authority;
mClient = client;
}
public RootInfo getRoot(String id) throws RemoteException {
final Uri rootsUri = DocumentsContract.buildRootsUri(mAuthority);
public RootInfo getRoot(String documentId) throws RemoteException {
final Uri rootsUri = buildRootsUri(mAuthority);
Cursor cursor = null;
try {
cursor = mClient.query(rootsUri, null, null, null, null);
while (cursor.moveToNext()) {
if (id.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
if (documentId.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
return RootInfo.fromRootsCursor(mAuthority, cursor);
}
}
throw new IllegalArgumentException("Can't find matching root for id=" + id);
throw new IllegalArgumentException("Can't find matching root for id=" + documentId);
} catch (Exception e) {
throw new RuntimeException("Can't load root for id=" + id , e);
throw new RuntimeException("Can't load root for id=" + documentId , e);
} finally {
IoUtils.closeQuietly(cursor);
}
@@ -67,22 +88,205 @@ public class DocumentsProviderHelper {
throw new IllegalArgumentException("Name and mimetype probably interposed.");
}
try {
return DocumentsContract.createDocument(mClient, parentUri, mimeType, name);
Uri uri = DocumentsContract.createDocument(mClient, parentUri, mimeType, name);
return uri;
} catch (RemoteException e) {
throw new RuntimeException("Couldn't create document: " + name + " with mimetype " + mimeType, e);
}
}
public Uri createDocument(String parentId, String mimeType, String name) {
Uri parentUri = buildDocumentUri(mAuthority, parentId);
return createDocument(parentUri, mimeType, name);
}
public Uri createDocument(RootInfo root, String mimeType, String name) {
return createDocument(root.documentId, mimeType, name);
}
public Uri createFolder(Uri parentUri, String name) {
return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
}
public Uri createDocument(RootInfo root, String mimeType, String name) {
Uri rootUri = DocumentsContract.buildDocumentUri(mAuthority, root.documentId);
return createDocument(rootUri, mimeType, name);
public Uri createFolder(String parentId, String name) {
Uri parentUri = buildDocumentUri(mAuthority, parentId);
return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
}
public Uri createFolder(RootInfo root, String name) {
return createDocument(root, Document.MIME_TYPE_DIR, name);
}
public void writeDocument(Uri documentUri, byte[] contents)
throws RemoteException, IOException {
ParcelFileDescriptor file = mClient.openFile(documentUri, "w", null);
try (AutoCloseOutputStream out = new AutoCloseOutputStream(file)) {
out.write(contents, 0, contents.length);
}
}
public byte[] readDocument(Uri documentUri) throws RemoteException, IOException {
Log.d("DocumentsProviderHelper", "Trying to read file contents: " + documentUri);
ParcelFileDescriptor file = mClient.openFile(documentUri, "r", null);
byte[] buf = null;
try (AutoCloseInputStream in = new AutoCloseInputStream(file)) {
buf = Streams.readFully(in);
}
return buf;
}
public void assertChildCount(Uri parentUri, int expected) throws Exception {
List<DocumentInfo> children = listChildren(parentUri);
assertEquals("Incorrect file count after copy", expected, children.size());
}
public void assertChildCount(String parentId, int expected) throws Exception {
List<DocumentInfo> children = listChildren(parentId);
assertEquals("Incorrect file count after copy", expected, children.size());
}
public void assertChildCount(RootInfo root, int expected) throws Exception {
assertChildCount(root.documentId, expected);
}
public void assertHasFile(Uri parentUri, String name) throws Exception {
List<DocumentInfo> children = listChildren(parentUri);
for (DocumentInfo child : children) {
if (name.equals(child.displayName) && !child.isDirectory()) {
return;
}
}
fail("Could not find file named=" + name + " in children " + children);
}
public void assertHasFile(String parentId, String name) throws Exception {
Uri parentUri = buildDocumentUri(mAuthority, parentId);
assertHasFile(parentUri, name);
}
public void assertHasFile(RootInfo root, String name) throws Exception {
assertHasFile(root.documentId, name);
}
public void assertHasDirectory(Uri parentUri, String name) throws Exception {
List<DocumentInfo> children = listChildren(parentUri);
for (DocumentInfo child : children) {
if (name.equals(child.displayName) && child.isDirectory()) {
return;
}
}
fail("Could not find name=" + name + " in children " + children);
}
public void assertHasDirectory(String parentId, String name) throws Exception {
Uri parentUri = buildDocumentUri(mAuthority, parentId);
assertHasDirectory(parentUri, name);
}
public void assertHasDirectory(RootInfo root, String name) throws Exception {
assertHasDirectory(root.documentId, name);
}
public void assertDoesNotExist(Uri parentUri, String name) throws Exception {
List<DocumentInfo> children = listChildren(parentUri);
for (DocumentInfo child : children) {
if (name.equals(child.displayName)) {
fail("Found name=" + name + " in children " + children);
}
}
}
public void assertDoesNotExist(String parentId, String name) throws Exception {
Uri parentUri = buildDocumentUri(mAuthority, parentId);
assertDoesNotExist(parentUri, name);
}
public void assertDoesNotExist(RootInfo root, String name) throws Exception {
assertDoesNotExist(root.getUri(), name);
}
public @Nullable DocumentInfo findFile(String parentId, String name)
throws Exception {
List<DocumentInfo> children = listChildren(parentId);
for (DocumentInfo child : children) {
if (name.equals(child.displayName)) {
return child;
}
}
return null;
}
public DocumentInfo findDocument(String parentId, String name) throws Exception {
List<DocumentInfo> children = listChildren(parentId);
for (DocumentInfo child : children) {
if (name.equals(child.displayName)) {
return child;
}
}
return null;
}
public DocumentInfo findDocument(Uri parentUri, String name) throws Exception {
List<DocumentInfo> children = listChildren(parentUri);
for (DocumentInfo child : children) {
if (name.equals(child.displayName)) {
return child;
}
}
return null;
}
public List<DocumentInfo> listChildren(Uri parentUri) throws Exception {
String id = DocumentsContract.getDocumentId(parentUri);
return listChildren(id);
}
public List<DocumentInfo> listChildren(String documentId) throws Exception {
Uri uri = buildChildDocumentsUri(mAuthority, documentId);
List<DocumentInfo> children = new ArrayList<>();
try (Cursor cursor = mClient.query(uri, null, null, null, null, null)) {
while (cursor.moveToNext()) {
children.add(DocumentInfo.fromDirectoryCursor(cursor));
}
}
return children;
}
public void assertFileContents(Uri documentUri, byte[] expected) throws Exception {
// TODO: Fix this: java.lang.SecurityException:
// The authority of the uri content:/document/%2Fdata%2Fuser%2F0%2Fcom.android.documentsui.\
// tests%2Fcache%2FTEST_ROOT_1%2Ftest1.txt does not match the one of the contentProvider: \
// com.android.documentsui.stubprovider
// MoreAsserts.assertEquals(
// "Copied file contents differ",
// expected, readDocument(documentUri));
}
public void assertFileContents(String parentId, String fileName, byte[] expected)
throws Exception {
DocumentInfo file = findFile(parentId, fileName);
assertNotNull(file);
assertFileContents(file.derivedUri, expected);
}
/**
* A helper method for StubProvider only. Won't work with other providers.
* @throws RemoteException
*/
public Uri createVirtualFile(
RootInfo root, String path, String mimeType, byte[] content, String... streamTypes)
throws RemoteException {
Bundle args = new Bundle();
args.putString(StubProvider.EXTRA_ROOT, root.rootId);
args.putString(StubProvider.EXTRA_PATH, path);
args.putString(Document.COLUMN_MIME_TYPE, mimeType);
args.putStringArrayList(StubProvider.EXTRA_STREAM_TYPES, Lists.newArrayList(streamTypes));
args.putByteArray(StubProvider.EXTRA_CONTENT, content);
Bundle result = mClient.call("createVirtualFile", null, args);
String documentId = result.getString(Document.COLUMN_DOCUMENT_ID);
return DocumentsContract.buildDocumentUri(mAuthority, documentId);
}
}

View File

@@ -48,8 +48,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class StubProvider extends DocumentsProvider {
@@ -57,9 +59,15 @@ public class StubProvider extends DocumentsProvider {
public static final String ROOT_0_ID = "TEST_ROOT_0";
public static final String ROOT_1_ID = "TEST_ROOT_1";
public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
public static final String EXTRA_STREAM_TYPES
= "com.android.documentsui.stubprovider.STREAM_TYPES";
public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
private static final String TAG = "StubProvider";
private static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
private static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
@@ -78,7 +86,7 @@ public class StubProvider extends DocumentsProvider {
private String mAuthority = DEFAULT_AUTHORITY;
private SharedPreferences mPrefs;
private String mSimulateReadErrors;
private Set<String> mSimulateReadErrorIds = new HashSet<>();
@Override
public void attachInfo(Context context, ProviderInfo info) {
@@ -97,6 +105,7 @@ public class StubProvider extends DocumentsProvider {
Log.d(TAG, "Resetting storage.");
removeChildrenRecursively(getContext().getCacheDir());
mStorage.clear();
mSimulateReadErrorIds.clear();
mPrefs = getContext().getSharedPreferences(
"com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
@@ -296,6 +305,7 @@ public class StubProvider extends DocumentsProvider {
@Override
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final StubDocument document = mStorage.get(docId);
if (document == null || !document.file.isFile()) {
throw new FileNotFoundException();
@@ -305,17 +315,12 @@ public class StubProvider extends DocumentsProvider {
}
if ("r".equals(mode)) {
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file,
ParcelFileDescriptor.MODE_READ_ONLY);
if (docId.equals(mSimulateReadErrors)) {
return new ParcelFileDescriptor(pfd) {
@Override
public void checkError() throws IOException {
throw new IOException("Test error");
}
};
if (mSimulateReadErrorIds.contains(docId)) {
Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
return ParcelFileDescriptor.open(
document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
}
return pfd;
return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
}
if ("w".equals(mode)) {
return startWrite(document);
@@ -326,7 +331,11 @@ public class StubProvider extends DocumentsProvider {
@VisibleForTesting
public void simulateReadErrorsForFile(Uri uri) {
mSimulateReadErrors = DocumentsContract.getDocumentId(uri);
simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
}
public void simulateReadErrorsForFile(String id) {
mSimulateReadErrorIds.add(id);
}
@Override
@@ -337,9 +346,9 @@ public class StubProvider extends DocumentsProvider {
@Override
public AssetFileDescriptor openTypedDocument(
String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
final StubDocument document = mStorage.get(documentId);
final StubDocument document = mStorage.get(docId);
if (document == null || !document.file.isFile() || document.streamTypes == null) {
throw new FileNotFoundException();
}
@@ -349,7 +358,7 @@ public class StubProvider extends DocumentsProvider {
if (mimeType.equals(mimeTypeFilter)) {
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
document.file, ParcelFileDescriptor.MODE_READ_ONLY);
if (documentId.equals(mSimulateReadErrors)) {
if (mSimulateReadErrorIds.contains(docId)) {
pfd = new ParcelFileDescriptor(pfd) {
@Override
public void checkError() throws IOException {
@@ -454,11 +463,36 @@ public class StubProvider extends DocumentsProvider {
case "configure":
configure(arg, extras);
return null;
case "createVirtualFile":
return createVirtualFileFromBundle(extras);
case "simulateReadErrorsForFile":
simulateReadErrorsForFile(arg);
return null;
default:
return super.call(method, arg, extras);
}
}
private Bundle createVirtualFileFromBundle(Bundle extras) {
try {
Uri uri = createVirtualFile(
extras.getString(EXTRA_ROOT),
extras.getString(EXTRA_PATH),
extras.getString(Document.COLUMN_MIME_TYPE),
extras.getStringArrayList(EXTRA_STREAM_TYPES),
extras.getByteArray(EXTRA_CONTENT));
String documentId = DocumentsContract.getDocumentId(uri);
Bundle result = new Bundle();
result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
return result;
} catch (IOException e) {
Log.e(TAG, "Couldn't create virtual file.");
}
return null;
}
private void configure(String arg, Bundle extras) {
Log.d(TAG, "Configure " + arg);
String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
@@ -530,6 +564,7 @@ public class StubProvider extends DocumentsProvider {
public Uri createVirtualFile(
String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
throws FileNotFoundException, IOException {
final File file = createFile(rootId, path, mimeType, content);
final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
if (parent == null) {
@@ -559,7 +594,7 @@ public class StubProvider extends DocumentsProvider {
private File createFile(String rootId, String path, String mimeType, byte[] content)
throws FileNotFoundException, IOException {
Log.d(TAG, "Creating test file " + rootId + ":" + path);
Log.d(TAG, "Creating test file " + rootId + " : " + path);
StubDocument root = mRoots.get(rootId).document;
if (root == null) {
throw new FileNotFoundException("No roots with the ID " + rootId + " were found");

View File

@@ -0,0 +1,210 @@
/*
* Copyright (C) 2015 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.StubProvider.ROOT_0_ID;
import static com.android.documentsui.StubProvider.ROOT_1_ID;
import static com.google.common.collect.Lists.newArrayList;
import android.content.ContentProviderClient;
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.DocumentsProviderHelper;
import com.android.documentsui.StubProvider;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import java.util.List;
@MediumTest
public abstract class BaseCopyJobTest extends AndroidTestCase {
static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
static final byte[] HAM_BYTES = "ham and cheese".getBytes();
static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
Context mContext;
ContentResolver mResolver;
ContentProviderClient mClient;
DocumentsProviderHelper mDocs;
TestJobListener mJobListener;
RootInfo mSrcRoot;
RootInfo mDestRoot;
@Override
protected void setUp() throws Exception {
super.setUp();
mJobListener = new TestJobListener();
// NOTE: Must be the "target" context, else security checks in content provider will fail.
mContext = getContext();
mResolver = mContext.getContentResolver();
mClient = mResolver.acquireContentProviderClient(AUTHORITY);
mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
initTestFiles();
}
@Override
protected void tearDown() throws Exception {
resetStorage();
mClient.release();
super.tearDown();
}
private void resetStorage() throws RemoteException {
mClient.call("clear", null, null);
}
private void initTestFiles() throws RemoteException {
mSrcRoot = mDocs.getRoot(ROOT_0_ID);
mDestRoot = mDocs.getRoot(ROOT_1_ID);
}
public void runCopyFilesTest() throws Exception {
Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
mDocs.writeDocument(testFile1, HAM_BYTES);
Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
mDocs.writeDocument(testFile2, FRUITY_BYTES);
createJob(newArrayList(testFile1, testFile2)).run();
mJobListener.waitForFinished();
mDocs.assertChildCount(mDestRoot, 2);
mDocs.assertHasFile(mDestRoot, "test1.txt");
mDocs.assertHasFile(mDestRoot, "test2.txt");
mDocs.assertFileContents(mDestRoot.documentId, "test1.txt", HAM_BYTES);
mDocs.assertFileContents(mDestRoot.documentId, "test2.txt", FRUITY_BYTES);
}
public void runCopyVirtualTypedFileTest() throws Exception {
Uri testFile = mDocs.createVirtualFile(
mSrcRoot, "/virtual.sth", "virtual/mime-type",
FRUITY_BYTES, "application/pdf", "text/html");
createJob(newArrayList(testFile)).run();
mJobListener.waitForFinished();
mDocs.assertChildCount(mDestRoot, 1);
mDocs.assertHasFile(mDestRoot, "virtual.sth.pdf"); // copy should convert file to PDF.
mDocs.assertFileContents(mDestRoot.documentId, "virtual.sth.pdf", FRUITY_BYTES);
}
public void runCopyVirtualNonTypedFileTest() throws Exception {
Uri testFile = mDocs.createVirtualFile(
mSrcRoot, "/virtual.sth", "virtual/mime-type",
FRUITY_BYTES);
createJob(newArrayList(testFile)).run();
mJobListener.waitForFinished();
mJobListener.assertFailed();
mJobListener.assertFilesFailed(newArrayList("virtual.sth"));
mDocs.assertChildCount(mDestRoot, 0);
}
public void runCopyEmptyDirTest() throws Exception {
Uri testDir = mDocs.createFolder(mSrcRoot, "emptyDir");
createJob(newArrayList(testDir)).run();
mJobListener.waitForFinished();
mDocs.assertChildCount(mDestRoot, 1);
mDocs.assertHasDirectory(mDestRoot, "emptyDir");
}
public void runCopyDirRecursivelyTest() throws Exception {
Uri testDir1 = mDocs.createFolder(mSrcRoot, "dir1");
mDocs.createDocument(testDir1, "text/plain", "test1.txt");
Uri testDir2 = mDocs.createFolder(testDir1, "dir2");
mDocs.createDocument(testDir2, "text/plain", "test2.txt");
createJob(newArrayList(testDir1)).run();
mJobListener.waitForFinished();
DocumentInfo dir1Copy = mDocs.findDocument(mDestRoot.documentId, "dir1");
mDocs.assertChildCount(dir1Copy.derivedUri, 2);
mDocs.assertHasDirectory(dir1Copy.derivedUri, "dir2");
mDocs.assertHasFile(dir1Copy.derivedUri, "test1.txt");
DocumentInfo dir2Copy = mDocs.findDocument(dir1Copy.documentId, "dir2");
mDocs.assertChildCount(dir2Copy.derivedUri, 1);
mDocs.assertHasFile(dir2Copy.derivedUri, "test2.txt");
}
public void runNoCopyDirToSelfTest() throws Exception {
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
createJob(newArrayList(testDir), testDir).run();
mJobListener.waitForFinished();
mJobListener.assertFailed();
mJobListener.assertFilesFailed(newArrayList("someDir"));
mDocs.assertChildCount(mDestRoot, 0);
}
public void runNoCopyDirToDescendentTest() throws Exception {
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
Uri descDir = mDocs.createFolder(testDir, "theDescendent");
createJob(newArrayList(testDir), descDir).run();
mJobListener.waitForFinished();
mJobListener.assertFailed();
mJobListener.assertFilesFailed(newArrayList("someDir"));
mDocs.assertChildCount(mDestRoot, 0);
}
public void runCopyFileWithReadErrorsTest() throws Exception {
Uri testFile = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
mDocs.writeDocument(testFile, HAM_BYTES);
String testId = DocumentsContract.getDocumentId(testFile);
mClient.call("simulateReadErrorsForFile", testId, null);
createJob(newArrayList(testFile)).run();
mJobListener.waitForFinished();
mJobListener.assertFailed();
mJobListener.assertFilesFailed(newArrayList("test1.txt"));
mDocs.assertChildCount(mDestRoot, 0);
}
final CopyJob createJob(List<Uri> srcs) throws Exception {
Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
return createJob(srcs, destination);
}
abstract CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception;
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2015 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 android.net.Uri;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.google.common.collect.Lists;
import java.util.List;
@MediumTest
public class CopyJobTest extends BaseCopyJobTest {
public void testCopyFiles() throws Exception {
runCopyFilesTest();
}
public void testCopyVirtualTypedFile() throws Exception {
runCopyVirtualTypedFileTest();
}
public void testCopyVirtualNonTypedFile() throws Exception {
runCopyVirtualNonTypedFileTest();
}
public void testCopyEmptyDir() throws Exception {
runCopyEmptyDirTest();
}
public void testCopyDirRecursively() throws Exception {
runCopyDirRecursivelyTest();
}
public void testNoCopyDirToSelf() throws Exception {
runNoCopyDirToSelfTest();
}
public void testNoCopyDirToDescendent() throws Exception {
runNoCopyDirToDescendentTest();
}
public void testCopyFileWithReadErrors() throws Exception {
runCopyFileWithReadErrorsTest();
}
@Override
CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, destination));
List<DocumentInfo> srcDocs = Lists.newArrayList();
for (Uri src : srcs) {
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
}
return new CopyJob(
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
}
}

View File

@@ -16,588 +16,171 @@
package com.android.documentsui.services;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.test.MoreAsserts;
import android.test.ServiceTestCase;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import static com.android.documentsui.services.FileOperations.createBaseIntent;
import static com.android.documentsui.services.FileOperations.createJobId;
import static com.google.android.collect.Lists.newArrayList;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.test.ServiceTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.DocumentsProviderHelper;
import com.android.documentsui.Shared;
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.Job.Listener;
import com.google.common.collect.Lists;
import libcore.io.IoUtils;
import libcore.io.Streams;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* TODO: Test progress updates.
*/
@MediumTest
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {
private static final DocumentInfo ALPHA_DOC = createDoc("alpha");
private static final DocumentInfo BETA_DOC = createDoc("alpha");
private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
private static final DocumentInfo DELTA_DOC = createDoc("delta");
private FileOperationService mService;
private TestScheduledExecutorService mExecutor;
private TestJobFactory mJobFactory;
public FileOperationServiceTest() {
super(FileOperationService.class);
}
private static String AUTHORITY = "com.android.documentsui.stubprovider";
private static String SRC_ROOT = StubProvider.ROOT_0_ID;
private static String DST_ROOT = StubProvider.ROOT_1_ID;
private static String TAG = "CopyTest";
private Context mContext;
private TestContentResolver mResolver;
private ContentProviderClient mClient;
private DocumentsProviderHelper mDocHelper;
private StubProvider mStorage;
private Context mSystemContext;
private CopyJobListener mListener;
@Override
protected void setUp() throws Exception {
super.setUp();
setupService(); // must be called first for our test setup to work correctly.
mListener = new CopyJobListener();
setupTestContext();
mClient = mResolver.acquireContentProviderClient(AUTHORITY);
mExecutor = new TestScheduledExecutorService();
mJobFactory = new TestJobFactory();
// Reset the stub provider's storage.
mStorage.clearCacheAndBuildRoots();
// Install test doubles.
mService = getService();
mDocHelper = new DocumentsProviderHelper(AUTHORITY, mClient);
assertNull(mService.executor);
mService.executor = mExecutor;
assertDestFileCount(0);
assertNull(mService.jobFactory);
mService.jobFactory = mJobFactory;
}
@Override
protected void tearDown() throws Exception {
mClient.release();
super.tearDown();
public void testRunsJobs() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
mExecutor.runAll();
mJobFactory.assertAllJobsStarted();
}
public void testCopyFile() throws Exception {
String srcPath = "/test0.txt";
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
"The five boxing wizards jump quickly".getBytes());
public void testRunsJobs_AfterFailure() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
startService(createCopyIntent(Lists.newArrayList(testFile)));
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
// 2 operations: file creation, then writing data.
mResolver.waitForChanges(2);
// Verify that one file was copied; check file contents.
assertDestFileCount(1);
assertCopied(srcPath);
mExecutor.runAll();
mJobFactory.assertAllJobsStarted();
}
public void testCopyVirtualTypedFile() throws Exception {
String srcPath = "/virtual.sth";
String expectedDstPath = "/virtual.sth.pdf";
ArrayList<String> streamTypes = new ArrayList<>();
streamTypes.add("application/pdf");
streamTypes.add("text/html");
String testContent = "I love fruit cakes!";
Uri testFile = mStorage.createVirtualFile(SRC_ROOT, srcPath, "virtual/mime-type",
streamTypes, testContent.getBytes());
public void testHoldsWakeLockWhileWorking() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(Lists.newArrayList(testFile)));
// 2 operations: file creation, then writing data.
mResolver.waitForChanges(2);
// Verify that one file was copied.
assertDestFileCount(1);
byte[] dstContent = readFile(DST_ROOT, expectedDstPath);
MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
assertTrue(mService.holdsWakeLock());
}
public void testMoveFile() throws Exception {
String srcPath = "/test0.txt";
String testContent = "The five boxing wizards jump quickly";
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
testContent.getBytes());
public void testReleasesWakeLock_AfterSuccess() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
moveIntent.putExtra(
FileOperationService.EXTRA_OPERATION,
FileOperationService.OPERATION_MOVE);
startService(moveIntent);
// 3 operations: file creation, writing data, deleting original.
mResolver.waitForChanges(3);
// Verify that one file was moved; check file contents.
assertDestFileCount(1);
assertDoesNotExist(SRC_ROOT, srcPath);
byte[] dstContent = readFile(DST_ROOT, srcPath);
MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
assertTrue(mService.holdsWakeLock());
mExecutor.runAll();
assertFalse(mService.holdsWakeLock());
}
public void testCopyMultipleFiles() throws Exception {
String testContent[] = {
"The five boxing wizards jump quickly",
"The quick brown fox jumps over the lazy dog",
"Jackdaws love my big sphinx of quartz"
};
String srcPaths[] = {
"/test0.txt",
"/test1.txt",
"/test2.txt"
};
List<Uri> testFiles = Lists.newArrayList(
mStorage.createRegularFile(SRC_ROOT, srcPaths[0], "text/plain",
testContent[0].getBytes()),
mStorage.createRegularFile(SRC_ROOT, srcPaths[1], "text/plain",
testContent[1].getBytes()),
mStorage.createRegularFile(SRC_ROOT, srcPaths[2], "text/plain",
testContent[2].getBytes()));
public void testReleasesWakeLock_AfterFailure() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
// Copy all the test files.
startService(createCopyIntent(testFiles));
// 3 file creations, 3 file writes.
mResolver.waitForChanges(6);
assertDestFileCount(3);
for (String path : srcPaths) {
assertCopied(path);
}
assertTrue(mService.holdsWakeLock());
mExecutor.runAll();
assertFalse(mService.holdsWakeLock());
}
public void testCopyEmptyDir() throws Exception {
String srcPath = "/emptyDir";
Uri testDir = createTestDirectory(srcPath);
public void testShutdownStopsExecutor_AfterSuccess() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(Lists.newArrayList(testDir)));
// Just 1 operation: Directory creation.
mResolver.waitForChanges(1);
assertDestFileCount(1);
// Verify that the dst exists and is a directory.
File dst = mStorage.getFile(DST_ROOT, srcPath);
assertTrue(dst.isDirectory());
mExecutor.isAlive();
mExecutor.runAll();
mExecutor.assertShutdown();
}
public void testNoCopyDirToSelf() throws Exception {
Uri testDir = createTestDirectory("/someDir");
public void testShutdownStopsExecutor_AfterMixedFailures() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
Intent intent = createCopyIntent(Lists.newArrayList(testDir), testDir);
startService(intent);
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
getService().addFinishedListener(mListener);
mListener.waitForFinished();
mListener.assertFailedCount(1);
mListener.assertFileFailed("someDir");
assertDestFileCount(0);
mExecutor.runAll();
mExecutor.assertShutdown();
}
public void testNoCopyDirToDescendent() throws Exception {
Uri testDir = createTestDirectory("/someDir");
Uri descDir = createTestDirectory("/someDir/theDescendent");
public void testShutdownStopsExecutor_AfterTotalFailure() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
Intent intent = createCopyIntent(Lists.newArrayList(testDir), descDir);
startService(intent);
getService().addFinishedListener(mListener);
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
mJobFactory.jobs.get(1).fail(GAMMA_DOC);
mListener.waitForFinished();
mListener.assertFailedCount(1);
mListener.assertFileFailed("someDir");
assertDestFileCount(0);
mExecutor.runAll();
mExecutor.assertShutdown();
}
public void testMoveEmptyDir() throws Exception {
String srcPath = "/emptyDir";
Uri testDir = createTestDirectory(srcPath);
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
startService(moveIntent);
// 2 operations: Directory creation, and removal of the original.
mResolver.waitForChanges(2);
assertDestFileCount(1);
// Verify that the dst exists and is a directory.
File dst = mStorage.getFile(DST_ROOT, srcPath);
assertTrue(dst.isDirectory());
// Verify that the src was cleaned up.
assertDoesNotExist(SRC_ROOT, srcPath);
}
public void testMovePopulatedDir() throws Exception {
String testContent[] = {
"The five boxing wizards jump quickly",
"The quick brown fox jumps over the lazy dog",
"Jackdaws love my big sphinx of quartz"
};
String srcDir = "/testdir";
String srcFiles[] = {
srcDir + "/test0.txt",
srcDir + "/test1.txt",
srcDir + "/test2.txt"
};
// Create test dir; put some files in it.
Uri testDir = createTestDirectory(srcDir);
mStorage.createRegularFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
mStorage.createRegularFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
mStorage.createRegularFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
startService(moveIntent);
// dir creation, then creation and writing of 3 files, then removal of src dir and 3 src
// files.
mResolver.waitForChanges(11);
// Check the content of the moved files.
File dst = mStorage.getFile(DST_ROOT, srcDir);
assertTrue(dst.isDirectory());
for (int i = 0; i < testContent.length; ++i) {
byte[] dstContent = readFile(DST_ROOT, srcFiles[i]);
MoreAsserts.assertEquals("Copied file contents differ", testContent[i].getBytes(),
dstContent);
}
// Check that the src files were removed.
assertDoesNotExist(SRC_ROOT, srcDir);
for (String srcFile : srcFiles) {
assertDoesNotExist(SRC_ROOT, srcFile);
}
}
public void testCopyFileWithReadErrors() throws Exception {
String srcPath = "/test0.txt";
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
"The five boxing wizards jump quickly".getBytes());
mStorage.simulateReadErrorsForFile(testFile);
startService(createCopyIntent(Lists.newArrayList(testFile)));
// 3 operations: file creation, writing, then deletion (due to failed copy).
mResolver.waitForChanges(3);
// Verify that the failed copy was cleaned up.
assertDestFileCount(0);
}
public void testCopyVirtualNonTypedFile() throws Exception {
String srcPath = "/non-typed.sth";
Uri testFile = mStorage.createVirtualFile(SRC_ROOT, srcPath, "virtual/mime-type",
null /* streamTypes */, "I love Tokyo!".getBytes());
Intent intent = createCopyIntent(Lists.newArrayList(testFile));
startService(intent);
getService().addFinishedListener(mListener);
mListener.waitForFinished();
mListener.assertFailedCount(1);
mListener.assertFileFailed("non-typed.sth");
assertDestFileCount(0);
}
public void testMoveFileWithReadErrors() throws Exception {
String srcPath = "/test0.txt";
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
"The five boxing wizards jump quickly".getBytes());
mStorage.simulateReadErrorsForFile(testFile);
Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
startService(moveIntent);
try {
// There should be 3 operations: file creation, writing, then deletion (due to failed
// copy). Wait for 4, in case the CopyService also attempts to do extra stuff (like
// delete the src file). This should time out.
mResolver.waitForChanges(4);
} catch (TimeoutException e) {
// Success path
return;
} finally {
// Verify that the failed copy was cleaned up, and the src file wasn't removed.
assertDestFileCount(0);
assertExists(SRC_ROOT, srcPath);
}
// The asserts above didn't fail, but the CopyService did something unexpected.
fail("Extra file operations were detected");
}
public void testMoveDirectoryWithReadErrors() throws Exception {
String testContent[] = {
"The five boxing wizards jump quickly",
"The quick brown fox jumps over the lazy dog",
"Jackdaws love my big sphinx of quartz"
};
String srcDir = "/testdir";
String srcFiles[] = {
srcDir + "/test0.txt",
srcDir + "/test1.txt",
srcDir + "/test2.txt"
};
// Create test dir; put some files in it.
Uri testDir = createTestDirectory(srcDir);
mStorage.createRegularFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
Uri errFile = mStorage
.createRegularFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
mStorage.createRegularFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
mStorage.simulateReadErrorsForFile(errFile);
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
startService(moveIntent);
// - dst dir creation,
// - creation and writing of 2 files, removal of 2 src files
// - creation and writing of 1 file, then removal of that file (due to error)
mResolver.waitForChanges(10);
// Check that both the src and dst dirs exist. The src dir shouldn't have been removed,
// because it should contain the one errFile.
assertTrue(mStorage.getFile(SRC_ROOT, srcDir).isDirectory());
assertTrue(mStorage.getFile(DST_ROOT, srcDir).isDirectory());
// Check the content of the moved files.
MoreAsserts.assertEquals("Copied file contents differ", testContent[0].getBytes(),
readFile(DST_ROOT, srcFiles[0]));
MoreAsserts.assertEquals("Copied file contents differ", testContent[2].getBytes(),
readFile(DST_ROOT, srcFiles[2]));
// Check that the src files were removed.
assertDoesNotExist(SRC_ROOT, srcFiles[0]);
assertDoesNotExist(SRC_ROOT, srcFiles[2]);
// Check that the error file was not copied over.
assertDoesNotExist(DST_ROOT, srcFiles[1]);
assertExists(SRC_ROOT, srcFiles[1]);
}
private Uri createTestDirectory(String dir) throws IOException {
return mStorage.createRegularFile(
SRC_ROOT, dir, DocumentsContract.Document.MIME_TYPE_DIR, null);
}
private Intent createCopyIntent(List<Uri> srcs) throws Exception {
RootInfo root = mDocHelper.getRoot(DST_ROOT);
final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, root.documentId);
return createCopyIntent(srcs, dst);
}
private Intent createCopyIntent(List<Uri> srcs, Uri dst) throws Exception {
final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
for (Uri src : srcs) {
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
}
private Intent createCopyIntent(ArrayList<DocumentInfo> files, DocumentInfo dest)
throws Exception {
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, dst));
final Intent copyIntent = new Intent(mContext, FileOperationService.class);
copyIntent.putExtra(
FileOperationService.EXTRA_OPERATION,
FileOperationService.OPERATION_COPY);
copyIntent.putExtra(
FileOperationService.EXTRA_JOB_ID,
FileOperationService.createJobId());
copyIntent.putParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST, srcDocs);
copyIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
stack.push(dest);
return copyIntent;
return createBaseIntent(OPERATION_COPY, getContext(), createJobId(), files, stack);
}
/**
* Returns a count of the files in the given directory.
*/
private void assertDestFileCount(int expected) throws RemoteException {
RootInfo dest = mDocHelper.getRoot(DST_ROOT);
final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
dest.documentId);
Cursor c = null;
int count = 0;
try {
c = mClient.query(queryUri, null, null, null, null);
count = c.getCount();
} finally {
IoUtils.closeQuietly(c);
}
assertEquals("Incorrect file count after copy", expected, count);
private static DocumentInfo createDoc(String name) {
// Doesn't need to be valid content Uri, just some urly looking thing.
Uri uri = new Uri.Builder()
.scheme("content")
.authority("com.android.documentsui.testing")
.path(name)
.build();
return createDoc(uri);
}
private void assertExists(String rootId, String path) throws Exception {
assertNotNull("An expected file was not found: " + path + " on root " + rootId,
mStorage.getFile(rootId, path));
private static DocumentInfo createDoc(Uri destination) {
DocumentInfo destDoc = new DocumentInfo();
destDoc.derivedUri = destination;
return destDoc;
}
private void assertDoesNotExist(String rootId, String path) throws Exception {
assertNull("Unexpected file found: " + path + " on root " + rootId,
mStorage.getFile(rootId, path));
}
private final class TestJobFactory extends Job.Factory {
private byte[] readFile(String rootId, String path) throws Exception {
File file = mStorage.getFile(rootId, path);
byte[] buf = null;
assertNotNull(file);
final List<TestJob> jobs = new ArrayList<>();
FileInputStream in = null;
try {
in = new FileInputStream(file);
buf = Streams.readFully(in);
} finally {
IoUtils.closeQuietly(in);
}
return buf;
}
private void assertCopied(String path) throws Exception {
MoreAsserts.assertEquals("Copied file contents differ", readFile(SRC_ROOT, path),
readFile(DST_ROOT, path));
}
/**
* Sets up a ContextWrapper that substitutes a stub NotificationManager. This allows the test to
* listen for notification events, to gauge copy progress.
*
* @throws FileNotFoundException
*/
private void setupTestContext() throws FileNotFoundException {
mSystemContext = getSystemContext();
// Set up the context with the test content resolver.
mResolver = new TestContentResolver();
mContext = new ContextWrapper(mSystemContext) {
@Override
public ContentResolver getContentResolver() {
return mResolver;
}
};
setContext(mContext);
// Create a local stub provider and add it to the content resolver.
ProviderInfo info = new ProviderInfo();
info.authority = AUTHORITY;
info.exported = true;
info.grantUriPermissions = true;
info.readPermission = android.Manifest.permission.MANAGE_DOCUMENTS;
info.writePermission = android.Manifest.permission.MANAGE_DOCUMENTS;
mStorage = new StubProvider();
mStorage.attachInfo(mContext, info);
mResolver.addProvider(AUTHORITY, mStorage);
}
private final class CopyJobListener implements FileOperationService.TestOnlyListener {
final CountDownLatch latch = new CountDownLatch(1);
final List<DocumentInfo> failedDocs = new ArrayList<>();
@Override
public void onFinished(List<DocumentInfo> failed) {
failedDocs.addAll(failed);
latch.countDown();
}
public void assertFileFailed(String expectedName) {
for (DocumentInfo failed : failedDocs) {
if (expectedName.equals(failed.displayName)) {
return;
}
}
fail("Couldn't find failed file: " + expectedName);
}
public void waitForFinished() throws InterruptedException {
latch.await(500, TimeUnit.MILLISECONDS);
}
public void assertFailedCount(int expected) {
assertEquals(expected, failedDocs.size());
}
}
/**
* A test resolver that enables this test suite to listen for notifications that mark when copy
* operations are done.
*/
class TestContentResolver extends MockContentResolver {
private CountDownLatch mReadySignal;
private CountDownLatch mNotificationSignal;
public TestContentResolver() {
mReadySignal = new CountDownLatch(1);
}
/**
* Wait for the given number of files to be copied to destination. Times out after 1 sec.
*/
public void waitForChanges(int count) throws Exception {
// Wait for no more than 1 second by default.
waitForChanges(count, 1000);
}
/**
* Wait for files to be copied to destination.
*
* @param count Number of files to wait for.
* @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out.
*/
public void waitForChanges(int count, int timeOut) throws Exception {
mNotificationSignal = new CountDownLatch(count);
// Signal that the test is now waiting for files.
mReadySignal.countDown();
if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Timed out waiting for file operations to complete.");
void assertAllJobsStarted() {
for (TestJob job : jobs) {
job.assertStarted();
}
}
@Override
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
// Wait until the test is ready to receive file notifications.
try {
mReadySignal.await();
} catch (InterruptedException e) {
Log.d(TAG, "Interrupted while waiting for file copy readiness");
Thread.currentThread().interrupt();
}
if (DocumentsContract.isDocumentUri(mContext, uri)) {
Log.d(TAG, "Notification: " + uri);
// Watch for document URI change notifications - this signifies the end of a copy.
mNotificationSignal.countDown();
}
Job createCopy(Context service, Context appContext, Listener listener, String id,
DocumentStack stack, List<DocumentInfo> srcs) {
TestJob job = new TestJob(service, appContext, listener, OPERATION_COPY, id, stack);
jobs.add(job);
return job;
}
};
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2015 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 android.net.Uri;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.google.common.collect.Lists;
import java.util.List;
@MediumTest
public class MoveJobTest extends BaseCopyJobTest {
public void testMoveFiles() throws Exception {
runCopyFilesTest();
mDocs.assertChildCount(mSrcRoot, 0);
}
public void testMoveVirtualTypedFile() throws Exception {
runCopyVirtualTypedFileTest();
mDocs.assertChildCount(mSrcRoot, 0);
}
public void testMoveVirtualNonTypedFile() throws Exception {
runCopyVirtualNonTypedFileTest();
// should have failed, source not deleted
mDocs.assertChildCount(mSrcRoot, 1);
}
public void testMoveEmptyDir() throws Exception {
runCopyEmptyDirTest();
mDocs.assertChildCount(mSrcRoot, 0);
}
public void testMoveDirRecursively() throws Exception {
runCopyDirRecursivelyTest();
mDocs.assertChildCount(mSrcRoot, 0);
}
public void testNoMoveDirToSelf() throws Exception {
runNoCopyDirToSelfTest();
// should have failed, source not deleted
mDocs.assertChildCount(mSrcRoot, 1);
}
public void testNoMoveDirToDescendent() throws Exception {
runNoCopyDirToDescendentTest();
// should have failed, source not deleted
mDocs.assertChildCount(mSrcRoot, 1);
}
public void testMoveFileWithReadErrors() throws Exception {
runCopyFileWithReadErrorsTest();
// should have failed, source not deleted
mDocs.assertChildCount(mSrcRoot, 1);
}
@Override
CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, destination));
List<DocumentInfo> srcDocs = Lists.newArrayList();
for (Uri src : srcs) {
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
}
return new MoveJob(
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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 android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.test.mock.MockContentResolver;
import android.util.Log;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* A test resolver that enables this test suite to listen for notifications that mark when copy
* operations are done.
*/
class TestContentResolver extends MockContentResolver {
private static final String TAG = "TestContextResolver";
private CountDownLatch mReadySignal;
private CountDownLatch mNotificationSignal;
private Context mContext;
public TestContentResolver(Context context) {
mContext = context;
mReadySignal = new CountDownLatch(1);
}
/**
* Wait for the given number of files to be copied to destination. Times out after 1 sec.
*/
public void waitForChanges(int count) throws Exception {
// Wait for no more than 1 second by default.
waitForChanges(count, 1000);
}
/**
* Wait for files to be copied to destination.
*
* @param count Number of files to wait for.
* @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out.
*/
public void waitForChanges(int count, int timeOut) throws Exception {
mNotificationSignal = new CountDownLatch(count);
// Signal that the test is now waiting for files.
mReadySignal.countDown();
if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Timed out waiting for file operations to complete.");
}
}
@Override
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
// Wait until the test is ready to receive file notifications.
try {
mReadySignal.await();
} catch (InterruptedException e) {
Log.d(TAG, "Interrupted while waiting for file copy readiness");
Thread.currentThread().interrupt();
}
if (DocumentsContract.isDocumentUri(mContext, uri)) {
Log.d(TAG, "Notification: " + uri);
// Watch for document URI change notifications - this signifies the end of a copy.
mNotificationSignal.countDown();
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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 junit.framework.Assert.assertTrue;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
import android.os.RemoteException;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
public class TestJob extends Job {
private boolean mStarted;
TestJob(
Context service, Context appContext, Listener listener,
int operationType, String id, DocumentStack stack) {
super(service, appContext, listener, operationType, id, stack);
}
@Override
void start() throws RemoteException {
mStarted = true;
}
void assertStarted() {
assertTrue(mStarted);
}
@Override
void cleanup() {}
void fail(DocumentInfo doc) {
onFileFailed(doc);
}
@Override
Notification getSetupNotification() {
return getSetupNotification(service.getString(R.string.copy_preparing));
}
@Override
Notification getFailureNotification() {
// the "copy" stuff was just convenient and available :)
return getFailureNotification(
R.plurals.copy_error_notification_title, R.drawable.ic_menu_copy);
}
@Override
Builder createProgressBuilder() {
// the "copy" stuff was just convenient and available :)
return super.createProgressBuilder(
service.getString(R.string.copy_notification_title),
R.drawable.ic_menu_copy,
service.getString(android.R.string.cancel),
R.drawable.ic_cab_cancel);
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
import com.android.documentsui.model.DocumentInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class TestJobListener implements Job.Listener {
private final CountDownLatch latch = new CountDownLatch(1);
private final List<Job> progress = new ArrayList<>();
@Nullable private Job started;
@Nullable private Job failed;
@Nullable private Job finished;
@Override
public void onStart(Job job) {
started = job;
}
@Override
public void onFailed(Job job) {
failed = job;
}
@Override
public void onFinished(Job job) {
this.finished = job;
latch.countDown();
}
@Override
public void onProgress(CopyJob job) {
progress.add(job);
}
public void assertStarted() {
if (started == null) {
fail("Job didn't start. onStart never called.");
}
}
public void assertFinished() {
if (finished == null) {
fail("Job didn't finish. onFinish never called.");
}
}
public void assertFailed() {
if (failed == null) {
fail("Job didn't fail. onFailed never called.");
}
}
public void assertFilesFailed(ArrayList<String> names) {
if (failed == null) {
fail("Can't test failed documetns. Job didn't fail.");
}
assertEquals(failed.failedFiles.size(), names.size());
for (String name : names) {
assertFileFailed(name);
}
}
public void assertFileFailed(String name) {
if (failed == null) {
fail("Can't test failed documetns. Job didn't fail.");
}
for (DocumentInfo failed : failed.failedFiles) {
if (name.equals(failed.displayName)) {
return;
}
}
fail("Couldn't find failed file: " + name);
}
public void assertCanceled() {
if (finished == null) {
fail("Can't determine if job was canceled. Job didn't finish.");
}
if (!finished.isCanceled()) {
fail("Job wasn't canceled. Job#isCanceled returned false.");
}
}
public void assertMadeProgress() {
if (progress.isEmpty()) {
fail("Job made no progress. onProgress never called.");
}
}
public void waitForFinished() throws InterruptedException {
latch.await(500, TimeUnit.MILLISECONDS);
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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 junit.framework.Assert.assertFalse;
import static junit.framework.Assert.fail;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class TestScheduledExecutorService implements ScheduledExecutorService {
private List<TestFuture> scheduled = new ArrayList<>();
private boolean shutdown;
@Override
public void shutdown() {
this.shutdown = true;
}
@Override
public List<Runnable> shutdownNow() {
this.shutdown = true;
return new ArrayList<>();
}
void assertShutdown() {
if (!shutdown) {
fail("Executor wasn't shut down.");
}
}
@Override
public boolean isShutdown() {
return shutdown;
}
@Override
public boolean isTerminated() {
throw new UnsupportedOperationException();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(Callable<T> task) {
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
throw new UnsupportedOperationException();
}
@Override
public Future<?> submit(Runnable task) {
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
throw new UnsupportedOperationException();
}
@Override
public void execute(Runnable command) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
TestFuture future = new TestFuture(command, delay, unit);
scheduled.add(future);
return future;
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,
TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
void runAll() {
for (TestFuture future : scheduled) {
future.runnable.run();
}
}
void run(int taskIndex) {
scheduled.get(taskIndex).runnable.run();
}
public void isAlive() {
assertFalse(isShutdown());
}
static class TestFuture implements ScheduledFuture<Void> {
final Runnable runnable;
final long delay;
final TimeUnit unit;
public TestFuture(Runnable runnable, long delay, TimeUnit unit) {
this.runnable = runnable;
this.delay = delay;
this.unit = unit;
}
@Override
public long getDelay(TimeUnit unit) {
return 0;
}
@Override
public int compareTo(Delayed arg0) {
return 0;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return false;
}
@Override
public Void get() throws InterruptedException, ExecutionException {
return null;
}
@Override
public Void get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return null;
}
}
}