diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java index 8f89b4e6e55ba..9707c9e33e9f4 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java @@ -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 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 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; } -} \ No newline at end of file + + private void onFileFailed(DocumentInfo file, String msg) { + Log.w(TAG, msg); + onFileFailed(file); + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java index 6d87ecf72cef2..1df20ac15cfa8 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java @@ -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 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 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... - if (mJobFinishedListener != null) { - mJobFinishedListener.onFinished(job.failedFiles); - } + List 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 srcs, DocumentStack stack) { - Job createJob( - @OpType int operationType, String id, ArrayList 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 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 failed); + @Override + public IBinder onBind(Intent intent) { + return null; // Boilerplate. See super#onBind } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java index 88bf03b496e76..0f1730a3e8bbe 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java @@ -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 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 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 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 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)); diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java index 5c37a87de90a4..c7939ebd8b185 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java @@ -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 srcs) { + return new CopyJob(service, appContext, listener, id, stack, srcs); + } + + Job createMove(Context service, Context appContext, Listener listener, + String id, DocumentStack stack, List 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); } } diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java index 4817f58d8d5a6..79440100689a4 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java @@ -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 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! } } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java b/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java index 7abc99c99a5a4..3c40b672cdccf 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/DocumentsProviderHelper.java @@ -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 children = listChildren(parentUri); + assertEquals("Incorrect file count after copy", expected, children.size()); + } + + public void assertChildCount(String parentId, int expected) throws Exception { + List 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 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 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 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 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 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 children = listChildren(parentUri); + for (DocumentInfo child : children) { + if (name.equals(child.displayName)) { + return child; + } + } + return null; + } + + public List listChildren(Uri parentUri) throws Exception { + String id = DocumentsContract.getDocumentId(parentUri); + return listChildren(id); + } + + public List listChildren(String documentId) throws Exception { + Uri uri = buildChildDocumentsUri(mAuthority, documentId); + List 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); + } } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java index fb6445bd1c551..a47d35017f26a 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java @@ -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 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 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"); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java new file mode 100644 index 0000000000000..f57ce53291683 --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/BaseCopyJobTest.java @@ -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 srcs) throws Exception { + Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId); + return createJob(srcs, destination); + } + + abstract CopyJob createJob(List srcs, Uri destination) throws Exception; +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java new file mode 100644 index 0000000000000..c0ce993f8d98e --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java @@ -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 srcs, Uri destination) throws Exception { + DocumentStack stack = new DocumentStack(); + stack.push(DocumentInfo.fromUri(mResolver, destination)); + + List srcDocs = Lists.newArrayList(); + for (Uri src : srcs) { + srcDocs.add(DocumentInfo.fromUri(mResolver, src)); + } + + return new CopyJob( + mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs); + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java index 35aad60b274ce..d55b6f0985481 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java @@ -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 { + 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 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 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 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 srcs, Uri dst) throws Exception { - final ArrayList srcDocs = Lists.newArrayList(); - for (Uri src : srcs) { - srcDocs.add(DocumentInfo.fromUri(mResolver, src)); - } - + private Intent createCopyIntent(ArrayList 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 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 failedDocs = new ArrayList<>(); - - @Override - public void onFinished(List 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 srcs) { + TestJob job = new TestJob(service, appContext, listener, OPERATION_COPY, id, stack); + jobs.add(job); + return job; } - }; + } } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java new file mode 100644 index 0000000000000..5e41524ccc12a --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java @@ -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 srcs, Uri destination) throws Exception { + DocumentStack stack = new DocumentStack(); + stack.push(DocumentInfo.fromUri(mResolver, destination)); + + List srcDocs = Lists.newArrayList(); + for (Uri src : srcs) { + srcDocs.add(DocumentInfo.fromUri(mResolver, src)); + } + + return new MoveJob( + mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs); + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestContentResolver.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestContentResolver.java new file mode 100644 index 0000000000000..8f39b1403074d --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestContentResolver.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java new file mode 100644 index 0000000000000..72da9a1b9e25a --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java @@ -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); + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJobListener.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJobListener.java new file mode 100644 index 0000000000000..01101970519cc --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJobListener.java @@ -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 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 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); + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java new file mode 100644 index 0000000000000..5c39b78cb60b2 --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java @@ -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 scheduled = new ArrayList<>(); + private boolean shutdown; + + @Override + public void shutdown() { + this.shutdown = true; + } + + @Override + public List 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 Future submit(Callable task) { + throw new UnsupportedOperationException(); + } + + @Override + public Future submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + public Future submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny(Collection> 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 ScheduledFuture schedule(Callable 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 { + + 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; + } + } +}