Merge "Support for concurrent file operations."
This commit is contained in:
@@ -17,6 +17,10 @@
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static android.os.SystemClock.elapsedRealtime;
|
||||
import static android.provider.DocumentsContract.buildChildDocumentsUri;
|
||||
import static android.provider.DocumentsContract.buildDocumentUri;
|
||||
import static android.provider.DocumentsContract.getDocumentId;
|
||||
import static android.provider.DocumentsContract.isChildDocument;
|
||||
import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
|
||||
import static com.android.documentsui.Shared.DEBUG;
|
||||
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
|
||||
@@ -44,6 +48,7 @@ import android.webkit.MimeTypeMap;
|
||||
import com.android.documentsui.R;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.DocumentStack;
|
||||
import com.android.documentsui.services.FileOperationService.OpType;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
@@ -80,9 +85,22 @@ class CopyJob extends Job {
|
||||
*
|
||||
* @param srcs List of files to be copied.
|
||||
*/
|
||||
CopyJob(Context serviceContext, Context appContext, Listener listener,
|
||||
CopyJob(Context service, Context appContext, Listener listener,
|
||||
String id, DocumentStack destination, List<DocumentInfo> srcs) {
|
||||
super(OPERATION_COPY, serviceContext, appContext, listener, id, destination);
|
||||
super(service, appContext, listener, OPERATION_COPY, id, destination);
|
||||
|
||||
checkArgument(!srcs.isEmpty());
|
||||
this.mSrcFiles = srcs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see @link {@link Job} constructor for most param descriptions.
|
||||
*
|
||||
* @param srcs List of files to be copied.
|
||||
*/
|
||||
CopyJob(Context service, Context appContext, Listener listener,
|
||||
@OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
|
||||
super(service, appContext, listener, opType, id, destination);
|
||||
|
||||
checkArgument(!srcs.isEmpty());
|
||||
this.mSrcFiles = srcs;
|
||||
@@ -91,15 +109,15 @@ class CopyJob extends Job {
|
||||
@Override
|
||||
Builder createProgressBuilder() {
|
||||
return super.createProgressBuilder(
|
||||
serviceContext.getString(R.string.copy_notification_title),
|
||||
service.getString(R.string.copy_notification_title),
|
||||
R.drawable.ic_menu_copy,
|
||||
serviceContext.getString(android.R.string.cancel),
|
||||
service.getString(android.R.string.cancel),
|
||||
R.drawable.ic_cab_cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification getSetupNotification() {
|
||||
return getSetupNotification(serviceContext.getString(R.string.copy_preparing));
|
||||
return getSetupNotification(service.getString(R.string.copy_preparing));
|
||||
}
|
||||
|
||||
public boolean shouldUpdateProgress() {
|
||||
@@ -113,7 +131,7 @@ class CopyJob extends Job {
|
||||
mProgressBuilder.setContentInfo(
|
||||
NumberFormat.getPercentInstance().format(completed));
|
||||
if (mRemainingTime > 0) {
|
||||
mProgressBuilder.setContentText(serviceContext.getString(msgId,
|
||||
mProgressBuilder.setContentText(service.getString(msgId,
|
||||
DateUtils.formatDuration(mRemainingTime)));
|
||||
} else {
|
||||
mProgressBuilder.setContentText(null);
|
||||
@@ -164,7 +182,7 @@ class CopyJob extends Job {
|
||||
}
|
||||
|
||||
@Override
|
||||
void run(FileOperationService service) throws RemoteException {
|
||||
void start() throws RemoteException {
|
||||
mStartTime = elapsedRealtime();
|
||||
|
||||
// Acquire content providers.
|
||||
@@ -186,16 +204,14 @@ class CopyJob extends Job {
|
||||
|
||||
// Guard unsupported recursive operation.
|
||||
if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
|
||||
if (DEBUG) Log.d(TAG, "Skipping recursive operation on directory "
|
||||
+ dstInfo.derivedUri);
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo,
|
||||
"Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DEBUG) Log.d(TAG,
|
||||
"Performing op-type:" + type() + " of " + srcInfo.displayName
|
||||
+ " (" + srcInfo.derivedUri + ")" + " to " + dstInfo.displayName
|
||||
+ " (" + dstInfo.derivedUri + ")");
|
||||
"Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
|
||||
+ " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
|
||||
|
||||
processDocument(srcInfo, dstInfo);
|
||||
}
|
||||
@@ -219,7 +235,6 @@ class CopyJob extends Job {
|
||||
*
|
||||
* @param srcInfo DocumentInfos for the documents to copy.
|
||||
* @param dstDirInfo The destination directory.
|
||||
* @param mode The transfer mode (copy or move).
|
||||
* @return True on success, false on failure.
|
||||
* @throws RemoteException
|
||||
*/
|
||||
@@ -234,7 +249,8 @@ class CopyJob extends Job {
|
||||
if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
|
||||
if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
|
||||
dstDirInfo.derivedUri) == null) {
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo,
|
||||
"Provider side copy failed for documents: " + srcInfo.derivedUri + ".");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -249,6 +265,7 @@ class CopyJob extends Job {
|
||||
final String dstMimeType;
|
||||
final String dstDisplayName;
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + srcInfo);
|
||||
// If the file is virtual, but can be converted to another format, then try to copy it
|
||||
// as such format. Also, append an extension for the target mime type (if known).
|
||||
if (srcInfo.isVirtualDocument()) {
|
||||
@@ -261,9 +278,7 @@ class CopyJob extends Job {
|
||||
dstDisplayName = srcInfo.displayName +
|
||||
(extension != null ? "." + extension : srcInfo.displayName);
|
||||
} else {
|
||||
// The virtual file is not available as any alternative streamable format.
|
||||
// TODO: Log failures.
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo, "Cannot copy virtual file. No streamable formats available.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -277,7 +292,9 @@ class CopyJob extends Job {
|
||||
dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
|
||||
if (dstUri == null) {
|
||||
// If this is a directory, the entire subdir will not be copied over.
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo,
|
||||
"Couldn't create destination document " + dstDisplayName
|
||||
+ " in directory " + dstDirInfo.displayName + ".");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -285,7 +302,8 @@ class CopyJob extends Job {
|
||||
try {
|
||||
dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
|
||||
} catch (FileNotFoundException e) {
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo,
|
||||
"Could not load DocumentInfo for newly created file: " + dstUri + ".");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -327,7 +345,7 @@ class CopyJob extends Job {
|
||||
srcDirInfo.documentId);
|
||||
cursor = srcClient.query(queryUri, queryColumns, null, null, null);
|
||||
DocumentInfo srcInfo;
|
||||
while (cursor.moveToNext()) {
|
||||
while (cursor.moveToNext() && !isCanceled()) {
|
||||
srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
|
||||
success &= processDocument(srcInfo, dstDirInfo);
|
||||
}
|
||||
@@ -374,7 +392,7 @@ class CopyJob extends Job {
|
||||
dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
|
||||
dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
int len;
|
||||
while ((len = src.read(buffer)) != -1) {
|
||||
if (isCanceled()) {
|
||||
@@ -389,7 +407,8 @@ class CopyJob extends Job {
|
||||
srcFile.checkError();
|
||||
} catch (IOException e) {
|
||||
success = false;
|
||||
onFileFailed(srcInfo);
|
||||
onFileFailed(srcInfo, "Exception thrown while copying from "
|
||||
+ srcInfo.derivedUri + " to " + dstInfo.derivedUri + ".");
|
||||
|
||||
if (dstFile != null) {
|
||||
try {
|
||||
@@ -405,7 +424,7 @@ class CopyJob extends Job {
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
// Clean up half-copied files.
|
||||
if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
|
||||
canceller.cancel();
|
||||
try {
|
||||
DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
|
||||
@@ -452,8 +471,7 @@ class CopyJob extends Job {
|
||||
private static long calculateFileSizesRecursively(
|
||||
ContentProviderClient client, Uri uri) throws RemoteException {
|
||||
final String authority = uri.getAuthority();
|
||||
final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
|
||||
DocumentsContract.getDocumentId(uri));
|
||||
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
|
||||
final String queryColumns[] = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
@@ -468,7 +486,7 @@ class CopyJob extends Job {
|
||||
if (Document.MIME_TYPE_DIR.equals(
|
||||
getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
|
||||
// Recurse into directories.
|
||||
final Uri dirUri = DocumentsContract.buildDocumentUri(authority,
|
||||
final Uri dirUri = buildDocumentUri(authority,
|
||||
getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
|
||||
result += calculateFileSizesRecursively(client, dirUri);
|
||||
} else {
|
||||
@@ -497,9 +515,13 @@ class CopyJob extends Job {
|
||||
boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
|
||||
throws RemoteException {
|
||||
if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
|
||||
return DocumentsContract.isChildDocument(
|
||||
dstClient, doc.derivedUri, parentDoc.derivedUri);
|
||||
return isChildDocument(dstClient, doc.derivedUri, parentDoc.derivedUri);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void onFileFailed(DocumentInfo file, String msg) {
|
||||
Log.w(TAG, msg);
|
||||
onFileFailed(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,36 +16,47 @@
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static android.os.SystemClock.elapsedRealtime;
|
||||
import static com.android.documentsui.Shared.DEBUG;
|
||||
import static com.android.internal.util.Preconditions.checkArgument;
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
import static com.android.internal.util.Preconditions.checkState;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.IntentService;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.documentsui.Shared;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.DocumentStack;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.android.documentsui.services.Job.Factory;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
|
||||
public class FileOperationService extends Service implements Job.Listener {
|
||||
|
||||
private static final int DEFAULT_DELAY = 0;
|
||||
private static final int MAX_DELAY = 10 * 1000; // ten seconds
|
||||
|
||||
public class FileOperationService extends IntentService implements Job.Listener {
|
||||
public static final String TAG = "FileOperationService";
|
||||
private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
|
||||
|
||||
public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
|
||||
public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
|
||||
public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
|
||||
public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
|
||||
public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
|
||||
@@ -68,23 +79,31 @@ public class FileOperationService extends IntentService implements Job.Listener
|
||||
// TODO: Move it to a shared file when more operations are implemented.
|
||||
public static final int FAILURE_COPY = 1;
|
||||
|
||||
private PowerManager mPowerManager;
|
||||
// The executor and job factory are visible for testing and non-final
|
||||
// so we'll have a way to inject test doubles from the test. It's
|
||||
// a sub-optimal arrangement.
|
||||
@VisibleForTesting ScheduledExecutorService executor;
|
||||
@VisibleForTesting Factory jobFactory;
|
||||
|
||||
private PowerManager mPowerManager;
|
||||
private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
|
||||
private NotificationManager mNotificationManager;
|
||||
|
||||
// TODO: Rework service to support multiple concurrent jobs.
|
||||
private volatile Job mJob;
|
||||
@GuardedBy("mRunning")
|
||||
private Map<String, JobRecord> mRunning = new HashMap<>();
|
||||
|
||||
// For testing only.
|
||||
@Nullable private TestOnlyListener mJobFinishedListener;
|
||||
|
||||
public FileOperationService() {
|
||||
super("FileOperationService");
|
||||
}
|
||||
private int mLastStarted;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
// Allow tests to pre-set these with test doubles.
|
||||
if (executor == null) {
|
||||
executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
|
||||
}
|
||||
|
||||
if (jobFactory == null) {
|
||||
jobFactory = Job.Factory.instance;
|
||||
}
|
||||
|
||||
if (DEBUG) Log.d(TAG, "Created.");
|
||||
mPowerManager = getSystemService(PowerManager.class);
|
||||
@@ -92,69 +111,50 @@ public class FileOperationService extends IntentService implements Job.Listener
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand: " + intent);
|
||||
if (intent.hasExtra(EXTRA_CANCEL)) {
|
||||
handleCancel(intent);
|
||||
return START_REDELIVER_INTENT;
|
||||
} else {
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onHandleIntent: " + intent);
|
||||
public int onStartCommand(Intent intent, int flags, int startTime) {
|
||||
// TODO: Ensure we're not being called with retry or redeliver.
|
||||
// checkArgument(flags == 0); // retry and redeliver are not supported.
|
||||
|
||||
String jobId = intent.getStringExtra(EXTRA_JOB_ID);
|
||||
@OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
|
||||
checkArgument(jobId != null);
|
||||
|
||||
if (intent.hasExtra(EXTRA_CANCEL)) {
|
||||
handleCancel(intent);
|
||||
return;
|
||||
} else {
|
||||
checkArgument(operationType != OPERATION_UNKNOWN);
|
||||
handleOperation(intent, startTime, jobId, operationType);
|
||||
}
|
||||
|
||||
checkArgument(operationType != OPERATION_UNKNOWN);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, TAG);
|
||||
private void handleOperation(Intent intent, int startTime, String jobId, int operationType) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with start time " + startTime);
|
||||
|
||||
ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
|
||||
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
|
||||
// Track start time so we can stop the service once we're out of work to do.
|
||||
mLastStarted = startTime;
|
||||
|
||||
Job job = createJob(operationType, jobId, srcs, stack);
|
||||
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
|
||||
mNotificationManager.notify(job.id, 0, job.getSetupNotification());
|
||||
job.run(this);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch-all to prevent any copy errors from wedging the app.
|
||||
Log.e(TAG, "Exceptions occurred during copying", e);
|
||||
} finally {
|
||||
if (DEBUG) Log.d(TAG, "Cleaning up after copy");
|
||||
|
||||
job.cleanup();
|
||||
wakeLock.release();
|
||||
|
||||
// Dismiss the ongoing copy notification when the copy is done.
|
||||
mNotificationManager.cancel(job.id, 0);
|
||||
|
||||
if (job.failed()) {
|
||||
Log.e(TAG, job.failedFiles.size() + " files failed to copy");
|
||||
mNotificationManager.notify(job.id, 0, job.getFailureNotification());
|
||||
Job job = null;
|
||||
synchronized (mRunning) {
|
||||
if (mWakeLock == null) {
|
||||
mWakeLock = mPowerManager.newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, TAG);
|
||||
}
|
||||
|
||||
// TEST ONLY CODE...<raised eyebrows>
|
||||
if (mJobFinishedListener != null) {
|
||||
mJobFinishedListener.onFinished(job.failedFiles);
|
||||
}
|
||||
List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
|
||||
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
|
||||
|
||||
deleteJob(job);
|
||||
if (DEBUG) Log.d(TAG, "Done cleaning up");
|
||||
job = createJob(operationType, jobId, srcs, stack);
|
||||
|
||||
mWakeLock.acquire();
|
||||
}
|
||||
|
||||
checkState(job != null);
|
||||
int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
|
||||
checkArgument(delay <= MAX_DELAY);
|
||||
ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
|
||||
mRunning.put(jobId, new JobRecord(job, future));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,12 +166,25 @@ public class FileOperationService extends IntentService implements Job.Listener
|
||||
checkArgument(intent.hasExtra(EXTRA_CANCEL));
|
||||
String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));
|
||||
|
||||
// Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
|
||||
// cancellation requests from affecting unrelated copy jobs. However, if the current job ID
|
||||
// is null, the service most likely crashed and was revived by the incoming cancel intent.
|
||||
// In that case, always allow the cancellation to proceed.
|
||||
if (mJob != null && Objects.equal(jobId, mJob.id)) {
|
||||
mJob.cancel();
|
||||
if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
|
||||
|
||||
synchronized (mRunning) {
|
||||
// Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
|
||||
// cancellation requests from affecting unrelated copy jobs. However, if the current job ID
|
||||
// is null, the service most likely crashed and was revived by the incoming cancel intent.
|
||||
// In that case, always allow the cancellation to proceed.
|
||||
JobRecord record = mRunning.get(jobId);
|
||||
if (record != null) {
|
||||
record.job.cancel();
|
||||
|
||||
// If the job hasn't been started, cancel it and explicitly clean up.
|
||||
// If it *has* been started, we wait for it to recognize this, then
|
||||
// allow it stop working in an orderly fashion.
|
||||
if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
|
||||
record.future.cancel(false);
|
||||
onFinished(record.job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss the progress notification here rather than in the copy loop. This preserves
|
||||
@@ -179,24 +192,23 @@ public class FileOperationService extends IntentService implements Job.Listener
|
||||
// Try to cancel it even if we don't have a job id...in case there is some sad
|
||||
// orphan notification.
|
||||
mNotificationManager.cancel(jobId, 0);
|
||||
|
||||
// TODO: Guarantee the job is being finalized
|
||||
}
|
||||
|
||||
public static String createJobId() {
|
||||
return String.valueOf(elapsedRealtime());
|
||||
}
|
||||
@GuardedBy("mRunning")
|
||||
private Job createJob(
|
||||
@OpType int operationType, String id, List<DocumentInfo> srcs, DocumentStack stack) {
|
||||
|
||||
Job createJob(
|
||||
@OpType int operationType, String id, ArrayList<DocumentInfo> srcs,
|
||||
DocumentStack stack) {
|
||||
|
||||
checkState(mJob == null);
|
||||
checkArgument(!mRunning.containsKey(id));
|
||||
|
||||
Job job = null;
|
||||
switch (operationType) {
|
||||
case OPERATION_COPY:
|
||||
mJob = new CopyJob(this, getApplicationContext(), this, id, stack, srcs);
|
||||
job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
|
||||
break;
|
||||
case OPERATION_MOVE:
|
||||
mJob = new MoveJob(this, getApplicationContext(), this, id, stack, srcs);
|
||||
job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs);
|
||||
break;
|
||||
case OPERATION_DELETE:
|
||||
throw new UnsupportedOperationException();
|
||||
@@ -204,42 +216,90 @@ public class FileOperationService extends IntentService implements Job.Listener
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
return checkNotNull(mJob);
|
||||
return checkNotNull(job);
|
||||
}
|
||||
|
||||
void deleteJob(Job job) {
|
||||
checkArgument(job == mJob);
|
||||
mJob = null;
|
||||
@GuardedBy("mRunning")
|
||||
private void deleteJob(Job job) {
|
||||
if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
|
||||
|
||||
JobRecord record = mRunning.remove(job.id);
|
||||
checkArgument(record != null);
|
||||
record.job.cleanup();
|
||||
|
||||
if (mRunning.isEmpty()) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Most likely shuts down. Won't shut down if service has a pending
|
||||
* message.
|
||||
*/
|
||||
private void shutdown() {
|
||||
if (DEBUG) Log.d(TAG, "Shutting down. Last start time: " + mLastStarted);
|
||||
mWakeLock.release();
|
||||
mWakeLock = null;
|
||||
boolean gonnaStop = stopSelfResult(mLastStarted);
|
||||
if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
|
||||
if (!gonnaStop) {
|
||||
Log.w(TAG, "Service should be stopping, but reports otherwise.");
|
||||
}
|
||||
// Sadly "gonnaStop" is always false in tests, so we can't guard executor shutdown.
|
||||
List<Runnable> unfinished = executor.shutdownNow();
|
||||
checkState(unfinished.isEmpty());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean holdsWakeLock() {
|
||||
return mWakeLock != null && mWakeLock.isHeld();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart(Job job) {
|
||||
if (DEBUG) Log.d(TAG, "onStart: " + job.id);
|
||||
mNotificationManager.notify(job.id, 0, job.getSetupNotification());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Job job) {
|
||||
if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
|
||||
|
||||
// Dismiss the ongoing copy notification when the copy is done.
|
||||
mNotificationManager.cancel(job.id, 0);
|
||||
|
||||
synchronized (mRunning) {
|
||||
deleteJob(job);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(CopyJob job) {
|
||||
if (DEBUG) Log.d(TAG, "On copy progress...");
|
||||
if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
|
||||
mNotificationManager.notify(job.id, 0, job.getProgressNotification());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(MoveJob job) {
|
||||
if (DEBUG) Log.d(TAG, "On move progress...");
|
||||
mNotificationManager.notify(job.id, 0, job.getProgressNotification());
|
||||
public void onFailed(Job job) {
|
||||
if (DEBUG) Log.d(TAG, "onFailed: " + job.id);
|
||||
checkArgument(job.failed());
|
||||
Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
|
||||
mNotificationManager.notify(job.id, 0, job.getFailureNotification());
|
||||
onFinished(job); // failed jobs don't call finished, so we do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be run when the next run job is finished.
|
||||
* This is test ONLY instrumentation. The alternative is for us to add
|
||||
* broadcast intents SOLELY for the purpose of testing.
|
||||
* @param listener
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void addFinishedListener(TestOnlyListener listener) {
|
||||
this.mJobFinishedListener = listener;
|
||||
private static final class JobRecord {
|
||||
private final Job job;
|
||||
private final ScheduledFuture<?> future;
|
||||
|
||||
public JobRecord(Job job, ScheduledFuture<?> future) {
|
||||
this.job = job;
|
||||
this.future = future;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used for testing. Is that obvious enough?
|
||||
*/
|
||||
@VisibleForTesting
|
||||
interface TestOnlyListener {
|
||||
void onFinished(List<DocumentInfo> failed);
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null; // Boilerplate. See super#onBind
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static android.os.SystemClock.elapsedRealtime;
|
||||
import static com.android.documentsui.Shared.DEBUG;
|
||||
import static com.android.documentsui.Shared.EXTRA_STACK;
|
||||
import static com.android.documentsui.Shared.asArrayList;
|
||||
@@ -29,6 +30,7 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_DE
|
||||
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Parcelable;
|
||||
@@ -52,6 +54,10 @@ public final class FileOperations {
|
||||
|
||||
private FileOperations() {}
|
||||
|
||||
public static String createJobId() {
|
||||
return String.valueOf(elapsedRealtime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to start the activity. Returns the job id.
|
||||
*/
|
||||
@@ -78,7 +84,7 @@ public final class FileOperations {
|
||||
*
|
||||
* @param context Context for the intent.
|
||||
* @param jobId The id of the job to cancel.
|
||||
* Use {@link FileOperationService#createJobId} if you don't have one handy.
|
||||
* Use {@link #createJobId} if you don't have one handy.
|
||||
* @param srcDocs A list of src files to copy.
|
||||
* @param dstStack The copy destination stack.
|
||||
*/
|
||||
@@ -97,13 +103,13 @@ public final class FileOperations {
|
||||
*
|
||||
* @param context Context for the intent.
|
||||
* @param jobId A unique jobid for this job.
|
||||
* Use {@link FileOperationService#createJobId} if you don't have one handy.
|
||||
* Use {@link #createJobId} if you don't have one handy.
|
||||
* @param srcDocs A list of src files to copy.
|
||||
* @param destination The copy destination stack.
|
||||
*/
|
||||
public static String copy(
|
||||
Activity activity, List<DocumentInfo> srcDocs, DocumentStack destination) {
|
||||
String jobId = FileOperationService.createJobId();
|
||||
String jobId = createJobId();
|
||||
if (DEBUG) Log.d(TAG, "Initiating 'copy' operation id: " + jobId);
|
||||
|
||||
Intent intent = createBaseIntent(OPERATION_COPY, activity, jobId, srcDocs, destination);
|
||||
@@ -120,13 +126,13 @@ public final class FileOperations {
|
||||
* Starts the service for a move operation.
|
||||
*
|
||||
* @param jobId A unique jobid for this job.
|
||||
* Use {@link FileOperationService#createJobId} if you don't have one handy.
|
||||
* Use {@link #createJobId} if you don't have one handy.
|
||||
* @param srcDocs A list of src files to copy.
|
||||
* @param destination The move destination stack.
|
||||
*/
|
||||
public static String move(
|
||||
Activity activity, List<DocumentInfo> srcDocs, DocumentStack destination) {
|
||||
String jobId = FileOperationService.createJobId();
|
||||
String jobId = createJobId();
|
||||
if (DEBUG) Log.d(TAG, "Initiating 'move' operation id: " + jobId);
|
||||
|
||||
Intent intent = createBaseIntent(OPERATION_MOVE, activity, jobId, srcDocs, destination);
|
||||
@@ -143,13 +149,13 @@ public final class FileOperations {
|
||||
* Starts the service for a move operation.
|
||||
*
|
||||
* @param jobId A unique jobid for this job.
|
||||
* Use {@link FileOperationService#createJobId} if you don't have one handy.
|
||||
* Use {@link #createJobId} if you don't have one handy.
|
||||
* @param srcDocs A list of src files to copy.
|
||||
* @return Id of the job.
|
||||
*/
|
||||
public static String delete(
|
||||
Activity activity, List<DocumentInfo> srcDocs, DocumentStack location) {
|
||||
String jobId = FileOperationService.createJobId();
|
||||
String jobId = createJobId();
|
||||
if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id: " + jobId);
|
||||
|
||||
Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, location);
|
||||
@@ -162,15 +168,15 @@ public final class FileOperations {
|
||||
* Starts the service for a move operation.
|
||||
*
|
||||
* @param jobId A unique jobid for this job.
|
||||
* Use {@link FileOperationService#createJobId} if you don't have one handy.
|
||||
* Use {@link #createJobId} if you don't have one handy.
|
||||
* @param srcDocs A list of src files to copy.
|
||||
* @return Id of the job.
|
||||
*/
|
||||
public static Intent createBaseIntent(
|
||||
@OpType int operationType, Activity activity, String jobId,
|
||||
@OpType int operationType, Context context, String jobId,
|
||||
List<DocumentInfo> srcDocs, DocumentStack localeStack) {
|
||||
|
||||
Intent intent = new Intent(activity, FileOperationService.class);
|
||||
Intent intent = new Intent(context, FileOperationService.class);
|
||||
intent.putExtra(EXTRA_JOB_ID, jobId);
|
||||
intent.putParcelableArrayListExtra(
|
||||
EXTRA_SRC_LIST, asArrayList(srcDocs));
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
|
||||
import static com.android.documentsui.services.FileOperationService.EXTRA_FAILURE;
|
||||
import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
|
||||
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
|
||||
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
|
||||
import static com.android.documentsui.services.FileOperationService.FAILURE_COPY;
|
||||
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
|
||||
import static com.android.internal.util.Preconditions.checkArgument;
|
||||
|
||||
@@ -39,14 +45,19 @@ import com.android.documentsui.model.DocumentStack;
|
||||
import com.android.documentsui.services.FileOperationService.OpType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
abstract class Job {
|
||||
/**
|
||||
* A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
|
||||
* to do work and show progress relating to this work.
|
||||
*/
|
||||
abstract class Job implements Runnable {
|
||||
|
||||
final Context serviceContext;
|
||||
final Context service;
|
||||
final Context appContext;
|
||||
final Listener listener;
|
||||
|
||||
final @OpType int mOpType;
|
||||
final @OpType int operationType;
|
||||
final String id;
|
||||
final DocumentStack stack;
|
||||
|
||||
@@ -58,10 +69,9 @@ abstract class Job {
|
||||
/**
|
||||
* A simple progressable job, much like an AsyncTask, but with support
|
||||
* for providing various related notification, progress and navigation information.
|
||||
* @param opType
|
||||
* @param operationType
|
||||
*
|
||||
* @param serviceContext The context of the service in which this job is running.
|
||||
* This is usually just "this".
|
||||
* @param service The service context in which this job is running.
|
||||
* @param appContext The context of the invoking application. This is usually
|
||||
* just {@code getApplicationContext()}.
|
||||
* @param listener
|
||||
@@ -70,14 +80,15 @@ abstract class Job {
|
||||
* destination in the Files app where the user will be take when the
|
||||
* navigation intent is invoked (presumably from notification).
|
||||
*/
|
||||
Job(@OpType int opType, Context serviceContext, Context appContext, Listener listener,
|
||||
String id, DocumentStack stack) {
|
||||
Job(Context service, Context appContext, Listener listener,
|
||||
@OpType int operationType, String id, DocumentStack stack) {
|
||||
|
||||
checkArgument(opType != OPERATION_UNKNOWN);
|
||||
this.serviceContext = serviceContext;
|
||||
checkArgument(operationType != OPERATION_UNKNOWN);
|
||||
|
||||
this.service = service;
|
||||
this.appContext = appContext;
|
||||
this.listener = listener;
|
||||
mOpType = opType;
|
||||
this.operationType = operationType;
|
||||
|
||||
this.id = id;
|
||||
this.stack = stack;
|
||||
@@ -85,13 +96,31 @@ abstract class Job {
|
||||
mProgressBuilder = createProgressBuilder();
|
||||
}
|
||||
|
||||
abstract void run(FileOperationService service) throws RemoteException;
|
||||
abstract void cleanup();
|
||||
|
||||
@OpType int type() {
|
||||
return mOpType;
|
||||
@Override
|
||||
public final void run() {
|
||||
listener.onStart(this);
|
||||
try {
|
||||
start();
|
||||
} catch (Exception e) {
|
||||
// In the case of an unmanaged failure, we still want
|
||||
// to resolve business in an orderly fashion. That'll
|
||||
// ensure the service is shut down and notifications
|
||||
// shown/closed.
|
||||
listener.onFailed(this);
|
||||
} finally {
|
||||
if (failed()) {
|
||||
listener.onFailed(this);
|
||||
} else {
|
||||
listener.onFinished(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract void start() throws RemoteException;
|
||||
|
||||
// Service will call this when it is done with the job.
|
||||
abstract void cleanup();
|
||||
|
||||
abstract Notification getSetupNotification();
|
||||
// TODO: Progress notification for deletes.
|
||||
// abstract Notification getProgressNotification(long bytesCopied);
|
||||
@@ -106,7 +135,7 @@ abstract class Job {
|
||||
}
|
||||
|
||||
final ContentResolver getContentResolver() {
|
||||
return serviceContext.getContentResolver();
|
||||
return service.getContentResolver();
|
||||
}
|
||||
|
||||
void onFileFailed(DocumentInfo file) {
|
||||
@@ -125,15 +154,15 @@ abstract class Job {
|
||||
|
||||
Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
|
||||
final Intent navigateIntent = buildNavigateIntent();
|
||||
navigateIntent.putExtra(FileOperationService.EXTRA_FAILURE, FileOperationService.FAILURE_COPY);
|
||||
navigateIntent.putExtra(FileOperationService.EXTRA_OPERATION, mOpType);
|
||||
navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
|
||||
navigateIntent.putExtra(EXTRA_OPERATION, operationType);
|
||||
|
||||
navigateIntent.putParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST, failedFiles);
|
||||
navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
|
||||
|
||||
final Notification.Builder errorBuilder = new Notification.Builder(serviceContext)
|
||||
.setContentTitle(serviceContext.getResources().getQuantityString(titleId,
|
||||
final Notification.Builder errorBuilder = new Notification.Builder(service)
|
||||
.setContentTitle(service.getResources().getQuantityString(titleId,
|
||||
failedFiles.size(), failedFiles.size()))
|
||||
.setContentText(serviceContext.getString(R.string.notification_touch_for_details))
|
||||
.setContentText(service.getString(R.string.notification_touch_for_details))
|
||||
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
|
||||
.setCategory(Notification.CATEGORY_ERROR)
|
||||
@@ -147,7 +176,7 @@ abstract class Job {
|
||||
final Builder createProgressBuilder(
|
||||
String title, @DrawableRes int icon,
|
||||
String actionTitle, @DrawableRes int actionIcon) {
|
||||
Notification.Builder progressBuilder = new Notification.Builder(serviceContext)
|
||||
Notification.Builder progressBuilder = new Notification.Builder(service)
|
||||
.setContentTitle(title)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(appContext, 0, buildNavigateIntent(), 0))
|
||||
@@ -161,7 +190,7 @@ abstract class Job {
|
||||
actionIcon,
|
||||
actionTitle,
|
||||
PendingIntent.getService(
|
||||
serviceContext,
|
||||
service,
|
||||
0,
|
||||
cancelIntent,
|
||||
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
|
||||
@@ -173,21 +202,44 @@ abstract class Job {
|
||||
* Creates an intent for navigating back to the destination directory.
|
||||
*/
|
||||
Intent buildNavigateIntent() {
|
||||
Intent intent = new Intent(serviceContext, FilesActivity.class);
|
||||
Intent intent = new Intent(service, FilesActivity.class);
|
||||
intent.setAction(DocumentsContract.ACTION_BROWSE);
|
||||
intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
|
||||
return intent;
|
||||
}
|
||||
|
||||
Intent createCancelIntent() {
|
||||
final Intent cancelIntent = new Intent(serviceContext, FileOperationService.class);
|
||||
cancelIntent.putExtra(FileOperationService.EXTRA_CANCEL, true);
|
||||
cancelIntent.putExtra(FileOperationService.EXTRA_JOB_ID, id);
|
||||
final Intent cancelIntent = new Intent(service, FileOperationService.class);
|
||||
cancelIntent.putExtra(EXTRA_CANCEL, true);
|
||||
cancelIntent.putExtra(EXTRA_JOB_ID, id);
|
||||
return cancelIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory class that facilitates our testing FileOperationService.
|
||||
*/
|
||||
static class Factory {
|
||||
|
||||
static final Factory instance = new Factory();
|
||||
|
||||
Job createCopy(Context service, Context appContext, Listener listener,
|
||||
String id, DocumentStack stack, List<DocumentInfo> srcs) {
|
||||
return new CopyJob(service, appContext, listener, id, stack, srcs);
|
||||
}
|
||||
|
||||
Job createMove(Context service, Context appContext, Listener listener,
|
||||
String id, DocumentStack stack, List<DocumentInfo> srcs) {
|
||||
return new MoveJob(service, appContext, listener, id, stack, srcs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener interface employed by the service that owns us as well as tests.
|
||||
*/
|
||||
interface Listener {
|
||||
void onStart(Job job);
|
||||
void onFailed(Job job);
|
||||
void onFinished(Job job);
|
||||
void onProgress(CopyJob job);
|
||||
void onProgress(MoveJob job);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Notification.Builder;
|
||||
import android.content.Context;
|
||||
@@ -43,28 +45,23 @@ final class MoveJob extends CopyJob {
|
||||
*
|
||||
* @param srcs List of files to be moved.
|
||||
*/
|
||||
MoveJob(Context serviceContext, Context appContext, Listener listener,
|
||||
MoveJob(Context service, Context appContext, Listener listener,
|
||||
String id, DocumentStack destination, List<DocumentInfo> srcs) {
|
||||
super(serviceContext, appContext, listener, id, destination, srcs);
|
||||
}
|
||||
|
||||
@Override
|
||||
int type() {
|
||||
return FileOperationService.OPERATION_MOVE;
|
||||
super(service, appContext, listener, OPERATION_MOVE, id, destination, srcs);
|
||||
}
|
||||
|
||||
@Override
|
||||
Builder createProgressBuilder() {
|
||||
return super.createProgressBuilder(
|
||||
serviceContext.getString(R.string.move_notification_title),
|
||||
service.getString(R.string.move_notification_title),
|
||||
R.drawable.ic_menu_copy,
|
||||
serviceContext.getString(android.R.string.cancel),
|
||||
service.getString(android.R.string.cancel),
|
||||
R.drawable.ic_cab_cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Notification getSetupNotification() {
|
||||
return getSetupNotification(serviceContext.getString(R.string.move_preparing));
|
||||
return getSetupNotification(service.getString(R.string.move_preparing));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,15 +75,6 @@ final class MoveJob extends CopyJob {
|
||||
R.plurals.move_error_notification_title, R.drawable.ic_menu_copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a the given document to the given location.
|
||||
*
|
||||
* @param srcInfo DocumentInfos for the documents to copy.
|
||||
* @param dstDirInfo The destination directory.
|
||||
* @param mode The transfer mode (copy or move).
|
||||
* @return True on success, false on failure.
|
||||
* @throws RemoteException
|
||||
*/
|
||||
@Override
|
||||
boolean processDocument(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
|
||||
|
||||
@@ -105,20 +93,22 @@ final class MoveJob extends CopyJob {
|
||||
}
|
||||
|
||||
// If we couldn't do an optimized copy...we fall back to vanilla byte copy.
|
||||
boolean success = byteCopyDocument(srcInfo, dstDirInfo);
|
||||
boolean copied = byteCopyDocument(srcInfo, dstDirInfo);
|
||||
|
||||
if (success) {
|
||||
// This is racey. We should make sure that we never delete a directory after
|
||||
// it changed, so we don't remove a file which had not been copied earlier
|
||||
// to the target location.
|
||||
try {
|
||||
DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
|
||||
return false;
|
||||
}
|
||||
return copied && !isCanceled() && deleteSrcDocument(srcInfo);
|
||||
}
|
||||
|
||||
private boolean deleteSrcDocument(DocumentInfo srcInfo) {
|
||||
// This is racey. We should make sure that we never delete a directory after
|
||||
// it changed, so we don't remove a file which had not been copied earlier
|
||||
// to the target location.
|
||||
try {
|
||||
DocumentsContract.deleteDocument(srcClient, srcInfo.derivedUri);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(TAG, "Failed to delete source after copy: " + srcInfo.derivedUri, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return success;
|
||||
return true; // victory dance!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,47 +16,68 @@
|
||||
|
||||
package com.android.documentsui;
|
||||
|
||||
import static android.provider.DocumentsContract.buildChildDocumentsUri;
|
||||
import static android.provider.DocumentsContract.buildDocumentUri;
|
||||
import static android.provider.DocumentsContract.buildRootsUri;
|
||||
import static com.android.documentsui.model.DocumentInfo.getCursorString;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static junit.framework.Assert.fail;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.test.MoreAsserts;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.RootInfo;
|
||||
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
import libcore.io.Streams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides support for creation of documents in a test settings.
|
||||
*/
|
||||
public class DocumentsProviderHelper {
|
||||
|
||||
private final ContentProviderClient mClient;
|
||||
private final String mAuthority;
|
||||
private final ContentProviderClient mClient;
|
||||
|
||||
public DocumentsProviderHelper(String authority, ContentProviderClient client) {
|
||||
mClient = client;
|
||||
mAuthority = authority;
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
public RootInfo getRoot(String id) throws RemoteException {
|
||||
final Uri rootsUri = DocumentsContract.buildRootsUri(mAuthority);
|
||||
public RootInfo getRoot(String documentId) throws RemoteException {
|
||||
final Uri rootsUri = buildRootsUri(mAuthority);
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = mClient.query(rootsUri, null, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
if (id.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
|
||||
if (documentId.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
|
||||
return RootInfo.fromRootsCursor(mAuthority, cursor);
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Can't find matching root for id=" + id);
|
||||
throw new IllegalArgumentException("Can't find matching root for id=" + documentId);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Can't load root for id=" + id , e);
|
||||
throw new RuntimeException("Can't load root for id=" + documentId , e);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(cursor);
|
||||
}
|
||||
@@ -67,22 +88,205 @@ public class DocumentsProviderHelper {
|
||||
throw new IllegalArgumentException("Name and mimetype probably interposed.");
|
||||
}
|
||||
try {
|
||||
return DocumentsContract.createDocument(mClient, parentUri, mimeType, name);
|
||||
Uri uri = DocumentsContract.createDocument(mClient, parentUri, mimeType, name);
|
||||
return uri;
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException("Couldn't create document: " + name + " with mimetype " + mimeType, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Uri createDocument(String parentId, String mimeType, String name) {
|
||||
Uri parentUri = buildDocumentUri(mAuthority, parentId);
|
||||
return createDocument(parentUri, mimeType, name);
|
||||
}
|
||||
|
||||
public Uri createDocument(RootInfo root, String mimeType, String name) {
|
||||
return createDocument(root.documentId, mimeType, name);
|
||||
}
|
||||
|
||||
public Uri createFolder(Uri parentUri, String name) {
|
||||
return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
|
||||
}
|
||||
|
||||
public Uri createDocument(RootInfo root, String mimeType, String name) {
|
||||
Uri rootUri = DocumentsContract.buildDocumentUri(mAuthority, root.documentId);
|
||||
return createDocument(rootUri, mimeType, name);
|
||||
public Uri createFolder(String parentId, String name) {
|
||||
Uri parentUri = buildDocumentUri(mAuthority, parentId);
|
||||
return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
|
||||
}
|
||||
|
||||
public Uri createFolder(RootInfo root, String name) {
|
||||
return createDocument(root, Document.MIME_TYPE_DIR, name);
|
||||
}
|
||||
|
||||
public void writeDocument(Uri documentUri, byte[] contents)
|
||||
throws RemoteException, IOException {
|
||||
ParcelFileDescriptor file = mClient.openFile(documentUri, "w", null);
|
||||
try (AutoCloseOutputStream out = new AutoCloseOutputStream(file)) {
|
||||
out.write(contents, 0, contents.length);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] readDocument(Uri documentUri) throws RemoteException, IOException {
|
||||
Log.d("DocumentsProviderHelper", "Trying to read file contents: " + documentUri);
|
||||
ParcelFileDescriptor file = mClient.openFile(documentUri, "r", null);
|
||||
byte[] buf = null;
|
||||
try (AutoCloseInputStream in = new AutoCloseInputStream(file)) {
|
||||
buf = Streams.readFully(in);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public void assertChildCount(Uri parentUri, int expected) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentUri);
|
||||
assertEquals("Incorrect file count after copy", expected, children.size());
|
||||
}
|
||||
|
||||
public void assertChildCount(String parentId, int expected) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentId);
|
||||
assertEquals("Incorrect file count after copy", expected, children.size());
|
||||
}
|
||||
|
||||
public void assertChildCount(RootInfo root, int expected) throws Exception {
|
||||
assertChildCount(root.documentId, expected);
|
||||
}
|
||||
|
||||
public void assertHasFile(Uri parentUri, String name) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentUri);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName) && !child.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Could not find file named=" + name + " in children " + children);
|
||||
}
|
||||
|
||||
public void assertHasFile(String parentId, String name) throws Exception {
|
||||
Uri parentUri = buildDocumentUri(mAuthority, parentId);
|
||||
assertHasFile(parentUri, name);
|
||||
}
|
||||
|
||||
public void assertHasFile(RootInfo root, String name) throws Exception {
|
||||
assertHasFile(root.documentId, name);
|
||||
}
|
||||
|
||||
public void assertHasDirectory(Uri parentUri, String name) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentUri);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName) && child.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Could not find name=" + name + " in children " + children);
|
||||
}
|
||||
|
||||
public void assertHasDirectory(String parentId, String name) throws Exception {
|
||||
Uri parentUri = buildDocumentUri(mAuthority, parentId);
|
||||
assertHasDirectory(parentUri, name);
|
||||
}
|
||||
|
||||
public void assertHasDirectory(RootInfo root, String name) throws Exception {
|
||||
assertHasDirectory(root.documentId, name);
|
||||
}
|
||||
|
||||
public void assertDoesNotExist(Uri parentUri, String name) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentUri);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName)) {
|
||||
fail("Found name=" + name + " in children " + children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void assertDoesNotExist(String parentId, String name) throws Exception {
|
||||
Uri parentUri = buildDocumentUri(mAuthority, parentId);
|
||||
assertDoesNotExist(parentUri, name);
|
||||
}
|
||||
|
||||
public void assertDoesNotExist(RootInfo root, String name) throws Exception {
|
||||
assertDoesNotExist(root.getUri(), name);
|
||||
}
|
||||
|
||||
public @Nullable DocumentInfo findFile(String parentId, String name)
|
||||
throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentId);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DocumentInfo findDocument(String parentId, String name) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentId);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DocumentInfo findDocument(Uri parentUri, String name) throws Exception {
|
||||
List<DocumentInfo> children = listChildren(parentUri);
|
||||
for (DocumentInfo child : children) {
|
||||
if (name.equals(child.displayName)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<DocumentInfo> listChildren(Uri parentUri) throws Exception {
|
||||
String id = DocumentsContract.getDocumentId(parentUri);
|
||||
return listChildren(id);
|
||||
}
|
||||
|
||||
public List<DocumentInfo> listChildren(String documentId) throws Exception {
|
||||
Uri uri = buildChildDocumentsUri(mAuthority, documentId);
|
||||
List<DocumentInfo> children = new ArrayList<>();
|
||||
try (Cursor cursor = mClient.query(uri, null, null, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
children.add(DocumentInfo.fromDirectoryCursor(cursor));
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
public void assertFileContents(Uri documentUri, byte[] expected) throws Exception {
|
||||
// TODO: Fix this: java.lang.SecurityException:
|
||||
// The authority of the uri content:/document/%2Fdata%2Fuser%2F0%2Fcom.android.documentsui.\
|
||||
// tests%2Fcache%2FTEST_ROOT_1%2Ftest1.txt does not match the one of the contentProvider: \
|
||||
// com.android.documentsui.stubprovider
|
||||
// MoreAsserts.assertEquals(
|
||||
// "Copied file contents differ",
|
||||
// expected, readDocument(documentUri));
|
||||
}
|
||||
|
||||
public void assertFileContents(String parentId, String fileName, byte[] expected)
|
||||
throws Exception {
|
||||
DocumentInfo file = findFile(parentId, fileName);
|
||||
assertNotNull(file);
|
||||
assertFileContents(file.derivedUri, expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method for StubProvider only. Won't work with other providers.
|
||||
* @throws RemoteException
|
||||
*/
|
||||
public Uri createVirtualFile(
|
||||
RootInfo root, String path, String mimeType, byte[] content, String... streamTypes)
|
||||
throws RemoteException {
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(StubProvider.EXTRA_ROOT, root.rootId);
|
||||
args.putString(StubProvider.EXTRA_PATH, path);
|
||||
args.putString(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
args.putStringArrayList(StubProvider.EXTRA_STREAM_TYPES, Lists.newArrayList(streamTypes));
|
||||
args.putByteArray(StubProvider.EXTRA_CONTENT, content);
|
||||
|
||||
Bundle result = mClient.call("createVirtualFile", null, args);
|
||||
String documentId = result.getString(Document.COLUMN_DOCUMENT_ID);
|
||||
|
||||
return DocumentsContract.buildDocumentUri(mAuthority, documentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,10 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class StubProvider extends DocumentsProvider {
|
||||
|
||||
@@ -57,9 +59,15 @@ public class StubProvider extends DocumentsProvider {
|
||||
public static final String ROOT_0_ID = "TEST_ROOT_0";
|
||||
public static final String ROOT_1_ID = "TEST_ROOT_1";
|
||||
|
||||
public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
|
||||
public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
|
||||
public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
|
||||
public static final String EXTRA_STREAM_TYPES
|
||||
= "com.android.documentsui.stubprovider.STREAM_TYPES";
|
||||
public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
|
||||
|
||||
private static final String TAG = "StubProvider";
|
||||
private static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
|
||||
private static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
|
||||
|
||||
private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
|
||||
private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB.
|
||||
|
||||
@@ -78,7 +86,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
|
||||
private String mAuthority = DEFAULT_AUTHORITY;
|
||||
private SharedPreferences mPrefs;
|
||||
private String mSimulateReadErrors;
|
||||
private Set<String> mSimulateReadErrorIds = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void attachInfo(Context context, ProviderInfo info) {
|
||||
@@ -97,6 +105,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
Log.d(TAG, "Resetting storage.");
|
||||
removeChildrenRecursively(getContext().getCacheDir());
|
||||
mStorage.clear();
|
||||
mSimulateReadErrorIds.clear();
|
||||
|
||||
mPrefs = getContext().getSharedPreferences(
|
||||
"com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
|
||||
@@ -296,6 +305,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException {
|
||||
|
||||
final StubDocument document = mStorage.get(docId);
|
||||
if (document == null || !document.file.isFile()) {
|
||||
throw new FileNotFoundException();
|
||||
@@ -305,17 +315,12 @@ public class StubProvider extends DocumentsProvider {
|
||||
}
|
||||
|
||||
if ("r".equals(mode)) {
|
||||
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
if (docId.equals(mSimulateReadErrors)) {
|
||||
return new ParcelFileDescriptor(pfd) {
|
||||
@Override
|
||||
public void checkError() throws IOException {
|
||||
throw new IOException("Test error");
|
||||
}
|
||||
};
|
||||
if (mSimulateReadErrorIds.contains(docId)) {
|
||||
Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
|
||||
return ParcelFileDescriptor.open(
|
||||
document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
|
||||
}
|
||||
return pfd;
|
||||
return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
if ("w".equals(mode)) {
|
||||
return startWrite(document);
|
||||
@@ -326,7 +331,11 @@ public class StubProvider extends DocumentsProvider {
|
||||
|
||||
@VisibleForTesting
|
||||
public void simulateReadErrorsForFile(Uri uri) {
|
||||
mSimulateReadErrors = DocumentsContract.getDocumentId(uri);
|
||||
simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
|
||||
}
|
||||
|
||||
public void simulateReadErrorsForFile(String id) {
|
||||
mSimulateReadErrorIds.add(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -337,9 +346,9 @@ public class StubProvider extends DocumentsProvider {
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openTypedDocument(
|
||||
String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
|
||||
String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
|
||||
throws FileNotFoundException {
|
||||
final StubDocument document = mStorage.get(documentId);
|
||||
final StubDocument document = mStorage.get(docId);
|
||||
if (document == null || !document.file.isFile() || document.streamTypes == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
@@ -349,7 +358,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
if (mimeType.equals(mimeTypeFilter)) {
|
||||
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
|
||||
document.file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
if (documentId.equals(mSimulateReadErrors)) {
|
||||
if (mSimulateReadErrorIds.contains(docId)) {
|
||||
pfd = new ParcelFileDescriptor(pfd) {
|
||||
@Override
|
||||
public void checkError() throws IOException {
|
||||
@@ -454,11 +463,36 @@ public class StubProvider extends DocumentsProvider {
|
||||
case "configure":
|
||||
configure(arg, extras);
|
||||
return null;
|
||||
case "createVirtualFile":
|
||||
return createVirtualFileFromBundle(extras);
|
||||
case "simulateReadErrorsForFile":
|
||||
simulateReadErrorsForFile(arg);
|
||||
return null;
|
||||
default:
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle createVirtualFileFromBundle(Bundle extras) {
|
||||
try {
|
||||
Uri uri = createVirtualFile(
|
||||
extras.getString(EXTRA_ROOT),
|
||||
extras.getString(EXTRA_PATH),
|
||||
extras.getString(Document.COLUMN_MIME_TYPE),
|
||||
extras.getStringArrayList(EXTRA_STREAM_TYPES),
|
||||
extras.getByteArray(EXTRA_CONTENT));
|
||||
|
||||
String documentId = DocumentsContract.getDocumentId(uri);
|
||||
Bundle result = new Bundle();
|
||||
result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Couldn't create virtual file.");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void configure(String arg, Bundle extras) {
|
||||
Log.d(TAG, "Configure " + arg);
|
||||
String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
|
||||
@@ -530,6 +564,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
public Uri createVirtualFile(
|
||||
String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
|
||||
throws FileNotFoundException, IOException {
|
||||
|
||||
final File file = createFile(rootId, path, mimeType, content);
|
||||
final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
|
||||
if (parent == null) {
|
||||
@@ -559,7 +594,7 @@ public class StubProvider extends DocumentsProvider {
|
||||
|
||||
private File createFile(String rootId, String path, String mimeType, byte[] content)
|
||||
throws FileNotFoundException, IOException {
|
||||
Log.d(TAG, "Creating test file " + rootId + ":" + path);
|
||||
Log.d(TAG, "Creating test file " + rootId + " : " + path);
|
||||
StubDocument root = mRoots.get(rootId).document;
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static com.android.documentsui.StubProvider.ROOT_0_ID;
|
||||
import static com.android.documentsui.StubProvider.ROOT_1_ID;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import com.android.documentsui.DocumentsProviderHelper;
|
||||
import com.android.documentsui.StubProvider;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.RootInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@MediumTest
|
||||
public abstract class BaseCopyJobTest extends AndroidTestCase {
|
||||
|
||||
static String AUTHORITY = StubProvider.DEFAULT_AUTHORITY;
|
||||
static final byte[] HAM_BYTES = "ham and cheese".getBytes();
|
||||
static final byte[] FRUITY_BYTES = "I love fruit cakes!".getBytes();
|
||||
|
||||
Context mContext;
|
||||
ContentResolver mResolver;
|
||||
ContentProviderClient mClient;
|
||||
DocumentsProviderHelper mDocs;
|
||||
TestJobListener mJobListener;
|
||||
RootInfo mSrcRoot;
|
||||
RootInfo mDestRoot;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mJobListener = new TestJobListener();
|
||||
|
||||
// NOTE: Must be the "target" context, else security checks in content provider will fail.
|
||||
mContext = getContext();
|
||||
mResolver = mContext.getContentResolver();
|
||||
|
||||
mClient = mResolver.acquireContentProviderClient(AUTHORITY);
|
||||
mDocs = new DocumentsProviderHelper(AUTHORITY, mClient);
|
||||
|
||||
initTestFiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
resetStorage();
|
||||
mClient.release();
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private void resetStorage() throws RemoteException {
|
||||
mClient.call("clear", null, null);
|
||||
}
|
||||
|
||||
private void initTestFiles() throws RemoteException {
|
||||
mSrcRoot = mDocs.getRoot(ROOT_0_ID);
|
||||
mDestRoot = mDocs.getRoot(ROOT_1_ID);
|
||||
}
|
||||
|
||||
public void runCopyFilesTest() throws Exception {
|
||||
Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
|
||||
mDocs.writeDocument(testFile1, HAM_BYTES);
|
||||
|
||||
Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
|
||||
mDocs.writeDocument(testFile2, FRUITY_BYTES);
|
||||
|
||||
createJob(newArrayList(testFile1, testFile2)).run();
|
||||
mJobListener.waitForFinished();
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 2);
|
||||
mDocs.assertHasFile(mDestRoot, "test1.txt");
|
||||
mDocs.assertHasFile(mDestRoot, "test2.txt");
|
||||
mDocs.assertFileContents(mDestRoot.documentId, "test1.txt", HAM_BYTES);
|
||||
mDocs.assertFileContents(mDestRoot.documentId, "test2.txt", FRUITY_BYTES);
|
||||
}
|
||||
|
||||
public void runCopyVirtualTypedFileTest() throws Exception {
|
||||
Uri testFile = mDocs.createVirtualFile(
|
||||
mSrcRoot, "/virtual.sth", "virtual/mime-type",
|
||||
FRUITY_BYTES, "application/pdf", "text/html");
|
||||
|
||||
createJob(newArrayList(testFile)).run();
|
||||
|
||||
mJobListener.waitForFinished();
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 1);
|
||||
mDocs.assertHasFile(mDestRoot, "virtual.sth.pdf"); // copy should convert file to PDF.
|
||||
mDocs.assertFileContents(mDestRoot.documentId, "virtual.sth.pdf", FRUITY_BYTES);
|
||||
}
|
||||
|
||||
public void runCopyVirtualNonTypedFileTest() throws Exception {
|
||||
Uri testFile = mDocs.createVirtualFile(
|
||||
mSrcRoot, "/virtual.sth", "virtual/mime-type",
|
||||
FRUITY_BYTES);
|
||||
|
||||
createJob(newArrayList(testFile)).run();
|
||||
|
||||
mJobListener.waitForFinished();
|
||||
mJobListener.assertFailed();
|
||||
mJobListener.assertFilesFailed(newArrayList("virtual.sth"));
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 0);
|
||||
}
|
||||
|
||||
public void runCopyEmptyDirTest() throws Exception {
|
||||
Uri testDir = mDocs.createFolder(mSrcRoot, "emptyDir");
|
||||
|
||||
createJob(newArrayList(testDir)).run();
|
||||
mJobListener.waitForFinished();
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 1);
|
||||
mDocs.assertHasDirectory(mDestRoot, "emptyDir");
|
||||
}
|
||||
|
||||
public void runCopyDirRecursivelyTest() throws Exception {
|
||||
|
||||
Uri testDir1 = mDocs.createFolder(mSrcRoot, "dir1");
|
||||
mDocs.createDocument(testDir1, "text/plain", "test1.txt");
|
||||
|
||||
Uri testDir2 = mDocs.createFolder(testDir1, "dir2");
|
||||
mDocs.createDocument(testDir2, "text/plain", "test2.txt");
|
||||
|
||||
createJob(newArrayList(testDir1)).run();
|
||||
mJobListener.waitForFinished();
|
||||
|
||||
DocumentInfo dir1Copy = mDocs.findDocument(mDestRoot.documentId, "dir1");
|
||||
|
||||
mDocs.assertChildCount(dir1Copy.derivedUri, 2);
|
||||
mDocs.assertHasDirectory(dir1Copy.derivedUri, "dir2");
|
||||
mDocs.assertHasFile(dir1Copy.derivedUri, "test1.txt");
|
||||
|
||||
DocumentInfo dir2Copy = mDocs.findDocument(dir1Copy.documentId, "dir2");
|
||||
mDocs.assertChildCount(dir2Copy.derivedUri, 1);
|
||||
mDocs.assertHasFile(dir2Copy.derivedUri, "test2.txt");
|
||||
}
|
||||
|
||||
public void runNoCopyDirToSelfTest() throws Exception {
|
||||
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
|
||||
|
||||
createJob(newArrayList(testDir), testDir).run();
|
||||
|
||||
mJobListener.waitForFinished();
|
||||
mJobListener.assertFailed();
|
||||
mJobListener.assertFilesFailed(newArrayList("someDir"));
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 0);
|
||||
}
|
||||
|
||||
public void runNoCopyDirToDescendentTest() throws Exception {
|
||||
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
|
||||
Uri descDir = mDocs.createFolder(testDir, "theDescendent");
|
||||
|
||||
createJob(newArrayList(testDir), descDir).run();
|
||||
|
||||
mJobListener.waitForFinished();
|
||||
mJobListener.assertFailed();
|
||||
mJobListener.assertFilesFailed(newArrayList("someDir"));
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 0);
|
||||
}
|
||||
|
||||
public void runCopyFileWithReadErrorsTest() throws Exception {
|
||||
Uri testFile = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
|
||||
mDocs.writeDocument(testFile, HAM_BYTES);
|
||||
|
||||
String testId = DocumentsContract.getDocumentId(testFile);
|
||||
mClient.call("simulateReadErrorsForFile", testId, null);
|
||||
|
||||
createJob(newArrayList(testFile)).run();
|
||||
|
||||
mJobListener.waitForFinished();
|
||||
mJobListener.assertFailed();
|
||||
mJobListener.assertFilesFailed(newArrayList("test1.txt"));
|
||||
|
||||
mDocs.assertChildCount(mDestRoot, 0);
|
||||
}
|
||||
|
||||
final CopyJob createJob(List<Uri> srcs) throws Exception {
|
||||
Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
|
||||
return createJob(srcs, destination);
|
||||
}
|
||||
|
||||
abstract CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.DocumentStack;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@MediumTest
|
||||
public class CopyJobTest extends BaseCopyJobTest {
|
||||
|
||||
public void testCopyFiles() throws Exception {
|
||||
runCopyFilesTest();
|
||||
}
|
||||
|
||||
public void testCopyVirtualTypedFile() throws Exception {
|
||||
runCopyVirtualTypedFileTest();
|
||||
}
|
||||
|
||||
public void testCopyVirtualNonTypedFile() throws Exception {
|
||||
runCopyVirtualNonTypedFileTest();
|
||||
}
|
||||
|
||||
public void testCopyEmptyDir() throws Exception {
|
||||
runCopyEmptyDirTest();
|
||||
}
|
||||
|
||||
public void testCopyDirRecursively() throws Exception {
|
||||
runCopyDirRecursivelyTest();
|
||||
}
|
||||
|
||||
public void testNoCopyDirToSelf() throws Exception {
|
||||
runNoCopyDirToSelfTest();
|
||||
}
|
||||
|
||||
public void testNoCopyDirToDescendent() throws Exception {
|
||||
runNoCopyDirToDescendentTest();
|
||||
}
|
||||
|
||||
public void testCopyFileWithReadErrors() throws Exception {
|
||||
runCopyFileWithReadErrorsTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
|
||||
DocumentStack stack = new DocumentStack();
|
||||
stack.push(DocumentInfo.fromUri(mResolver, destination));
|
||||
|
||||
List<DocumentInfo> srcDocs = Lists.newArrayList();
|
||||
for (Uri src : srcs) {
|
||||
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
|
||||
}
|
||||
|
||||
return new CopyJob(
|
||||
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
|
||||
}
|
||||
}
|
||||
@@ -16,588 +16,171 @@
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.test.MoreAsserts;
|
||||
import android.test.ServiceTestCase;
|
||||
import android.test.mock.MockContentResolver;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
import android.util.Log;
|
||||
import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
|
||||
import static com.android.documentsui.services.FileOperations.createBaseIntent;
|
||||
import static com.android.documentsui.services.FileOperations.createJobId;
|
||||
import static com.google.android.collect.Lists.newArrayList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.test.ServiceTestCase;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import com.android.documentsui.DocumentsProviderHelper;
|
||||
import com.android.documentsui.Shared;
|
||||
import com.android.documentsui.StubProvider;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.DocumentStack;
|
||||
import com.android.documentsui.model.RootInfo;
|
||||
import com.android.documentsui.services.Job.Listener;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
import libcore.io.Streams;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* TODO: Test progress updates.
|
||||
*/
|
||||
@MediumTest
|
||||
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {
|
||||
|
||||
private static final DocumentInfo ALPHA_DOC = createDoc("alpha");
|
||||
private static final DocumentInfo BETA_DOC = createDoc("alpha");
|
||||
private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
|
||||
private static final DocumentInfo DELTA_DOC = createDoc("delta");
|
||||
|
||||
private FileOperationService mService;
|
||||
private TestScheduledExecutorService mExecutor;
|
||||
private TestJobFactory mJobFactory;
|
||||
|
||||
public FileOperationServiceTest() {
|
||||
super(FileOperationService.class);
|
||||
}
|
||||
|
||||
private static String AUTHORITY = "com.android.documentsui.stubprovider";
|
||||
private static String SRC_ROOT = StubProvider.ROOT_0_ID;
|
||||
private static String DST_ROOT = StubProvider.ROOT_1_ID;
|
||||
private static String TAG = "CopyTest";
|
||||
|
||||
private Context mContext;
|
||||
private TestContentResolver mResolver;
|
||||
private ContentProviderClient mClient;
|
||||
private DocumentsProviderHelper mDocHelper;
|
||||
private StubProvider mStorage;
|
||||
private Context mSystemContext;
|
||||
private CopyJobListener mListener;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
setupService(); // must be called first for our test setup to work correctly.
|
||||
|
||||
mListener = new CopyJobListener();
|
||||
setupTestContext();
|
||||
mClient = mResolver.acquireContentProviderClient(AUTHORITY);
|
||||
mExecutor = new TestScheduledExecutorService();
|
||||
mJobFactory = new TestJobFactory();
|
||||
|
||||
// Reset the stub provider's storage.
|
||||
mStorage.clearCacheAndBuildRoots();
|
||||
// Install test doubles.
|
||||
mService = getService();
|
||||
|
||||
mDocHelper = new DocumentsProviderHelper(AUTHORITY, mClient);
|
||||
assertNull(mService.executor);
|
||||
mService.executor = mExecutor;
|
||||
|
||||
assertDestFileCount(0);
|
||||
assertNull(mService.jobFactory);
|
||||
mService.jobFactory = mJobFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
mClient.release();
|
||||
super.tearDown();
|
||||
public void testRunsJobs() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
|
||||
|
||||
mExecutor.runAll();
|
||||
mJobFactory.assertAllJobsStarted();
|
||||
}
|
||||
|
||||
public void testCopyFile() throws Exception {
|
||||
String srcPath = "/test0.txt";
|
||||
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
|
||||
"The five boxing wizards jump quickly".getBytes());
|
||||
public void testRunsJobs_AfterFailure() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
|
||||
|
||||
startService(createCopyIntent(Lists.newArrayList(testFile)));
|
||||
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
|
||||
|
||||
// 2 operations: file creation, then writing data.
|
||||
mResolver.waitForChanges(2);
|
||||
|
||||
// Verify that one file was copied; check file contents.
|
||||
assertDestFileCount(1);
|
||||
assertCopied(srcPath);
|
||||
mExecutor.runAll();
|
||||
mJobFactory.assertAllJobsStarted();
|
||||
}
|
||||
|
||||
public void testCopyVirtualTypedFile() throws Exception {
|
||||
String srcPath = "/virtual.sth";
|
||||
String expectedDstPath = "/virtual.sth.pdf";
|
||||
ArrayList<String> streamTypes = new ArrayList<>();
|
||||
streamTypes.add("application/pdf");
|
||||
streamTypes.add("text/html");
|
||||
String testContent = "I love fruit cakes!";
|
||||
Uri testFile = mStorage.createVirtualFile(SRC_ROOT, srcPath, "virtual/mime-type",
|
||||
streamTypes, testContent.getBytes());
|
||||
public void testHoldsWakeLockWhileWorking() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
|
||||
startService(createCopyIntent(Lists.newArrayList(testFile)));
|
||||
|
||||
// 2 operations: file creation, then writing data.
|
||||
mResolver.waitForChanges(2);
|
||||
|
||||
// Verify that one file was copied.
|
||||
assertDestFileCount(1);
|
||||
|
||||
byte[] dstContent = readFile(DST_ROOT, expectedDstPath);
|
||||
MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
|
||||
assertTrue(mService.holdsWakeLock());
|
||||
}
|
||||
|
||||
public void testMoveFile() throws Exception {
|
||||
String srcPath = "/test0.txt";
|
||||
String testContent = "The five boxing wizards jump quickly";
|
||||
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
|
||||
testContent.getBytes());
|
||||
public void testReleasesWakeLock_AfterSuccess() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
|
||||
Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
|
||||
moveIntent.putExtra(
|
||||
FileOperationService.EXTRA_OPERATION,
|
||||
FileOperationService.OPERATION_MOVE);
|
||||
startService(moveIntent);
|
||||
|
||||
// 3 operations: file creation, writing data, deleting original.
|
||||
mResolver.waitForChanges(3);
|
||||
|
||||
// Verify that one file was moved; check file contents.
|
||||
assertDestFileCount(1);
|
||||
assertDoesNotExist(SRC_ROOT, srcPath);
|
||||
|
||||
byte[] dstContent = readFile(DST_ROOT, srcPath);
|
||||
MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
|
||||
assertTrue(mService.holdsWakeLock());
|
||||
mExecutor.runAll();
|
||||
assertFalse(mService.holdsWakeLock());
|
||||
}
|
||||
|
||||
public void testCopyMultipleFiles() throws Exception {
|
||||
String testContent[] = {
|
||||
"The five boxing wizards jump quickly",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
"Jackdaws love my big sphinx of quartz"
|
||||
};
|
||||
String srcPaths[] = {
|
||||
"/test0.txt",
|
||||
"/test1.txt",
|
||||
"/test2.txt"
|
||||
};
|
||||
List<Uri> testFiles = Lists.newArrayList(
|
||||
mStorage.createRegularFile(SRC_ROOT, srcPaths[0], "text/plain",
|
||||
testContent[0].getBytes()),
|
||||
mStorage.createRegularFile(SRC_ROOT, srcPaths[1], "text/plain",
|
||||
testContent[1].getBytes()),
|
||||
mStorage.createRegularFile(SRC_ROOT, srcPaths[2], "text/plain",
|
||||
testContent[2].getBytes()));
|
||||
public void testReleasesWakeLock_AfterFailure() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
|
||||
// Copy all the test files.
|
||||
startService(createCopyIntent(testFiles));
|
||||
|
||||
// 3 file creations, 3 file writes.
|
||||
mResolver.waitForChanges(6);
|
||||
|
||||
assertDestFileCount(3);
|
||||
for (String path : srcPaths) {
|
||||
assertCopied(path);
|
||||
}
|
||||
assertTrue(mService.holdsWakeLock());
|
||||
mExecutor.runAll();
|
||||
assertFalse(mService.holdsWakeLock());
|
||||
}
|
||||
|
||||
public void testCopyEmptyDir() throws Exception {
|
||||
String srcPath = "/emptyDir";
|
||||
Uri testDir = createTestDirectory(srcPath);
|
||||
public void testShutdownStopsExecutor_AfterSuccess() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
|
||||
startService(createCopyIntent(Lists.newArrayList(testDir)));
|
||||
|
||||
// Just 1 operation: Directory creation.
|
||||
mResolver.waitForChanges(1);
|
||||
|
||||
assertDestFileCount(1);
|
||||
|
||||
// Verify that the dst exists and is a directory.
|
||||
File dst = mStorage.getFile(DST_ROOT, srcPath);
|
||||
assertTrue(dst.isDirectory());
|
||||
mExecutor.isAlive();
|
||||
mExecutor.runAll();
|
||||
mExecutor.assertShutdown();
|
||||
}
|
||||
|
||||
public void testNoCopyDirToSelf() throws Exception {
|
||||
Uri testDir = createTestDirectory("/someDir");
|
||||
public void testShutdownStopsExecutor_AfterMixedFailures() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
|
||||
|
||||
Intent intent = createCopyIntent(Lists.newArrayList(testDir), testDir);
|
||||
startService(intent);
|
||||
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
|
||||
|
||||
getService().addFinishedListener(mListener);
|
||||
|
||||
mListener.waitForFinished();
|
||||
mListener.assertFailedCount(1);
|
||||
mListener.assertFileFailed("someDir");
|
||||
|
||||
assertDestFileCount(0);
|
||||
mExecutor.runAll();
|
||||
mExecutor.assertShutdown();
|
||||
}
|
||||
|
||||
public void testNoCopyDirToDescendent() throws Exception {
|
||||
Uri testDir = createTestDirectory("/someDir");
|
||||
Uri descDir = createTestDirectory("/someDir/theDescendent");
|
||||
public void testShutdownStopsExecutor_AfterTotalFailure() throws Exception {
|
||||
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
|
||||
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
|
||||
|
||||
Intent intent = createCopyIntent(Lists.newArrayList(testDir), descDir);
|
||||
startService(intent);
|
||||
getService().addFinishedListener(mListener);
|
||||
mJobFactory.jobs.get(0).fail(ALPHA_DOC);
|
||||
mJobFactory.jobs.get(1).fail(GAMMA_DOC);
|
||||
|
||||
mListener.waitForFinished();
|
||||
mListener.assertFailedCount(1);
|
||||
mListener.assertFileFailed("someDir");
|
||||
|
||||
assertDestFileCount(0);
|
||||
mExecutor.runAll();
|
||||
mExecutor.assertShutdown();
|
||||
}
|
||||
|
||||
public void testMoveEmptyDir() throws Exception {
|
||||
String srcPath = "/emptyDir";
|
||||
Uri testDir = createTestDirectory(srcPath);
|
||||
|
||||
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
|
||||
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
|
||||
startService(moveIntent);
|
||||
|
||||
// 2 operations: Directory creation, and removal of the original.
|
||||
mResolver.waitForChanges(2);
|
||||
|
||||
assertDestFileCount(1);
|
||||
|
||||
// Verify that the dst exists and is a directory.
|
||||
File dst = mStorage.getFile(DST_ROOT, srcPath);
|
||||
assertTrue(dst.isDirectory());
|
||||
|
||||
// Verify that the src was cleaned up.
|
||||
assertDoesNotExist(SRC_ROOT, srcPath);
|
||||
}
|
||||
|
||||
public void testMovePopulatedDir() throws Exception {
|
||||
String testContent[] = {
|
||||
"The five boxing wizards jump quickly",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
"Jackdaws love my big sphinx of quartz"
|
||||
};
|
||||
String srcDir = "/testdir";
|
||||
String srcFiles[] = {
|
||||
srcDir + "/test0.txt",
|
||||
srcDir + "/test1.txt",
|
||||
srcDir + "/test2.txt"
|
||||
};
|
||||
// Create test dir; put some files in it.
|
||||
Uri testDir = createTestDirectory(srcDir);
|
||||
mStorage.createRegularFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
|
||||
mStorage.createRegularFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
|
||||
mStorage.createRegularFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
|
||||
|
||||
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
|
||||
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
|
||||
startService(moveIntent);
|
||||
|
||||
// dir creation, then creation and writing of 3 files, then removal of src dir and 3 src
|
||||
// files.
|
||||
mResolver.waitForChanges(11);
|
||||
|
||||
// Check the content of the moved files.
|
||||
File dst = mStorage.getFile(DST_ROOT, srcDir);
|
||||
assertTrue(dst.isDirectory());
|
||||
for (int i = 0; i < testContent.length; ++i) {
|
||||
byte[] dstContent = readFile(DST_ROOT, srcFiles[i]);
|
||||
MoreAsserts.assertEquals("Copied file contents differ", testContent[i].getBytes(),
|
||||
dstContent);
|
||||
}
|
||||
|
||||
// Check that the src files were removed.
|
||||
assertDoesNotExist(SRC_ROOT, srcDir);
|
||||
for (String srcFile : srcFiles) {
|
||||
assertDoesNotExist(SRC_ROOT, srcFile);
|
||||
}
|
||||
}
|
||||
|
||||
public void testCopyFileWithReadErrors() throws Exception {
|
||||
String srcPath = "/test0.txt";
|
||||
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
|
||||
"The five boxing wizards jump quickly".getBytes());
|
||||
|
||||
mStorage.simulateReadErrorsForFile(testFile);
|
||||
|
||||
startService(createCopyIntent(Lists.newArrayList(testFile)));
|
||||
|
||||
// 3 operations: file creation, writing, then deletion (due to failed copy).
|
||||
mResolver.waitForChanges(3);
|
||||
|
||||
// Verify that the failed copy was cleaned up.
|
||||
assertDestFileCount(0);
|
||||
}
|
||||
|
||||
public void testCopyVirtualNonTypedFile() throws Exception {
|
||||
String srcPath = "/non-typed.sth";
|
||||
Uri testFile = mStorage.createVirtualFile(SRC_ROOT, srcPath, "virtual/mime-type",
|
||||
null /* streamTypes */, "I love Tokyo!".getBytes());
|
||||
|
||||
Intent intent = createCopyIntent(Lists.newArrayList(testFile));
|
||||
startService(intent);
|
||||
getService().addFinishedListener(mListener);
|
||||
|
||||
mListener.waitForFinished();
|
||||
mListener.assertFailedCount(1);
|
||||
mListener.assertFileFailed("non-typed.sth");
|
||||
assertDestFileCount(0);
|
||||
}
|
||||
|
||||
public void testMoveFileWithReadErrors() throws Exception {
|
||||
String srcPath = "/test0.txt";
|
||||
Uri testFile = mStorage.createRegularFile(SRC_ROOT, srcPath, "text/plain",
|
||||
"The five boxing wizards jump quickly".getBytes());
|
||||
|
||||
mStorage.simulateReadErrorsForFile(testFile);
|
||||
|
||||
Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
|
||||
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
|
||||
startService(moveIntent);
|
||||
|
||||
try {
|
||||
// There should be 3 operations: file creation, writing, then deletion (due to failed
|
||||
// copy). Wait for 4, in case the CopyService also attempts to do extra stuff (like
|
||||
// delete the src file). This should time out.
|
||||
mResolver.waitForChanges(4);
|
||||
} catch (TimeoutException e) {
|
||||
// Success path
|
||||
return;
|
||||
} finally {
|
||||
// Verify that the failed copy was cleaned up, and the src file wasn't removed.
|
||||
assertDestFileCount(0);
|
||||
assertExists(SRC_ROOT, srcPath);
|
||||
}
|
||||
// The asserts above didn't fail, but the CopyService did something unexpected.
|
||||
fail("Extra file operations were detected");
|
||||
}
|
||||
|
||||
public void testMoveDirectoryWithReadErrors() throws Exception {
|
||||
String testContent[] = {
|
||||
"The five boxing wizards jump quickly",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
"Jackdaws love my big sphinx of quartz"
|
||||
};
|
||||
String srcDir = "/testdir";
|
||||
String srcFiles[] = {
|
||||
srcDir + "/test0.txt",
|
||||
srcDir + "/test1.txt",
|
||||
srcDir + "/test2.txt"
|
||||
};
|
||||
// Create test dir; put some files in it.
|
||||
Uri testDir = createTestDirectory(srcDir);
|
||||
mStorage.createRegularFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes());
|
||||
Uri errFile = mStorage
|
||||
.createRegularFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes());
|
||||
mStorage.createRegularFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes());
|
||||
|
||||
mStorage.simulateReadErrorsForFile(errFile);
|
||||
|
||||
Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
|
||||
moveIntent.putExtra(FileOperationService.EXTRA_OPERATION, FileOperationService.OPERATION_MOVE);
|
||||
startService(moveIntent);
|
||||
|
||||
// - dst dir creation,
|
||||
// - creation and writing of 2 files, removal of 2 src files
|
||||
// - creation and writing of 1 file, then removal of that file (due to error)
|
||||
mResolver.waitForChanges(10);
|
||||
|
||||
// Check that both the src and dst dirs exist. The src dir shouldn't have been removed,
|
||||
// because it should contain the one errFile.
|
||||
assertTrue(mStorage.getFile(SRC_ROOT, srcDir).isDirectory());
|
||||
assertTrue(mStorage.getFile(DST_ROOT, srcDir).isDirectory());
|
||||
|
||||
// Check the content of the moved files.
|
||||
MoreAsserts.assertEquals("Copied file contents differ", testContent[0].getBytes(),
|
||||
readFile(DST_ROOT, srcFiles[0]));
|
||||
MoreAsserts.assertEquals("Copied file contents differ", testContent[2].getBytes(),
|
||||
readFile(DST_ROOT, srcFiles[2]));
|
||||
|
||||
// Check that the src files were removed.
|
||||
assertDoesNotExist(SRC_ROOT, srcFiles[0]);
|
||||
assertDoesNotExist(SRC_ROOT, srcFiles[2]);
|
||||
|
||||
// Check that the error file was not copied over.
|
||||
assertDoesNotExist(DST_ROOT, srcFiles[1]);
|
||||
assertExists(SRC_ROOT, srcFiles[1]);
|
||||
}
|
||||
|
||||
private Uri createTestDirectory(String dir) throws IOException {
|
||||
return mStorage.createRegularFile(
|
||||
SRC_ROOT, dir, DocumentsContract.Document.MIME_TYPE_DIR, null);
|
||||
}
|
||||
|
||||
private Intent createCopyIntent(List<Uri> srcs) throws Exception {
|
||||
RootInfo root = mDocHelper.getRoot(DST_ROOT);
|
||||
final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, root.documentId);
|
||||
|
||||
return createCopyIntent(srcs, dst);
|
||||
}
|
||||
|
||||
private Intent createCopyIntent(List<Uri> srcs, Uri dst) throws Exception {
|
||||
final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
|
||||
for (Uri src : srcs) {
|
||||
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
|
||||
}
|
||||
|
||||
private Intent createCopyIntent(ArrayList<DocumentInfo> files, DocumentInfo dest)
|
||||
throws Exception {
|
||||
DocumentStack stack = new DocumentStack();
|
||||
stack.push(DocumentInfo.fromUri(mResolver, dst));
|
||||
final Intent copyIntent = new Intent(mContext, FileOperationService.class);
|
||||
copyIntent.putExtra(
|
||||
FileOperationService.EXTRA_OPERATION,
|
||||
FileOperationService.OPERATION_COPY);
|
||||
copyIntent.putExtra(
|
||||
FileOperationService.EXTRA_JOB_ID,
|
||||
FileOperationService.createJobId());
|
||||
copyIntent.putParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST, srcDocs);
|
||||
copyIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
|
||||
stack.push(dest);
|
||||
|
||||
return copyIntent;
|
||||
return createBaseIntent(OPERATION_COPY, getContext(), createJobId(), files, stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the files in the given directory.
|
||||
*/
|
||||
private void assertDestFileCount(int expected) throws RemoteException {
|
||||
RootInfo dest = mDocHelper.getRoot(DST_ROOT);
|
||||
final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
|
||||
dest.documentId);
|
||||
Cursor c = null;
|
||||
int count = 0;
|
||||
try {
|
||||
c = mClient.query(queryUri, null, null, null, null);
|
||||
count = c.getCount();
|
||||
} finally {
|
||||
IoUtils.closeQuietly(c);
|
||||
}
|
||||
assertEquals("Incorrect file count after copy", expected, count);
|
||||
private static DocumentInfo createDoc(String name) {
|
||||
// Doesn't need to be valid content Uri, just some urly looking thing.
|
||||
Uri uri = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("com.android.documentsui.testing")
|
||||
.path(name)
|
||||
.build();
|
||||
|
||||
return createDoc(uri);
|
||||
}
|
||||
|
||||
private void assertExists(String rootId, String path) throws Exception {
|
||||
assertNotNull("An expected file was not found: " + path + " on root " + rootId,
|
||||
mStorage.getFile(rootId, path));
|
||||
private static DocumentInfo createDoc(Uri destination) {
|
||||
DocumentInfo destDoc = new DocumentInfo();
|
||||
destDoc.derivedUri = destination;
|
||||
return destDoc;
|
||||
}
|
||||
|
||||
private void assertDoesNotExist(String rootId, String path) throws Exception {
|
||||
assertNull("Unexpected file found: " + path + " on root " + rootId,
|
||||
mStorage.getFile(rootId, path));
|
||||
}
|
||||
private final class TestJobFactory extends Job.Factory {
|
||||
|
||||
private byte[] readFile(String rootId, String path) throws Exception {
|
||||
File file = mStorage.getFile(rootId, path);
|
||||
byte[] buf = null;
|
||||
assertNotNull(file);
|
||||
final List<TestJob> jobs = new ArrayList<>();
|
||||
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(file);
|
||||
buf = Streams.readFully(in);
|
||||
} finally {
|
||||
IoUtils.closeQuietly(in);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private void assertCopied(String path) throws Exception {
|
||||
MoreAsserts.assertEquals("Copied file contents differ", readFile(SRC_ROOT, path),
|
||||
readFile(DST_ROOT, path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a ContextWrapper that substitutes a stub NotificationManager. This allows the test to
|
||||
* listen for notification events, to gauge copy progress.
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
private void setupTestContext() throws FileNotFoundException {
|
||||
mSystemContext = getSystemContext();
|
||||
|
||||
// Set up the context with the test content resolver.
|
||||
mResolver = new TestContentResolver();
|
||||
mContext = new ContextWrapper(mSystemContext) {
|
||||
@Override
|
||||
public ContentResolver getContentResolver() {
|
||||
return mResolver;
|
||||
}
|
||||
};
|
||||
setContext(mContext);
|
||||
|
||||
// Create a local stub provider and add it to the content resolver.
|
||||
ProviderInfo info = new ProviderInfo();
|
||||
info.authority = AUTHORITY;
|
||||
info.exported = true;
|
||||
info.grantUriPermissions = true;
|
||||
info.readPermission = android.Manifest.permission.MANAGE_DOCUMENTS;
|
||||
info.writePermission = android.Manifest.permission.MANAGE_DOCUMENTS;
|
||||
|
||||
mStorage = new StubProvider();
|
||||
mStorage.attachInfo(mContext, info);
|
||||
mResolver.addProvider(AUTHORITY, mStorage);
|
||||
}
|
||||
|
||||
private final class CopyJobListener implements FileOperationService.TestOnlyListener {
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final List<DocumentInfo> failedDocs = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onFinished(List<DocumentInfo> failed) {
|
||||
failedDocs.addAll(failed);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public void assertFileFailed(String expectedName) {
|
||||
for (DocumentInfo failed : failedDocs) {
|
||||
if (expectedName.equals(failed.displayName)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Couldn't find failed file: " + expectedName);
|
||||
}
|
||||
|
||||
public void waitForFinished() throws InterruptedException {
|
||||
latch.await(500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void assertFailedCount(int expected) {
|
||||
assertEquals(expected, failedDocs.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A test resolver that enables this test suite to listen for notifications that mark when copy
|
||||
* operations are done.
|
||||
*/
|
||||
class TestContentResolver extends MockContentResolver {
|
||||
private CountDownLatch mReadySignal;
|
||||
private CountDownLatch mNotificationSignal;
|
||||
|
||||
public TestContentResolver() {
|
||||
mReadySignal = new CountDownLatch(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the given number of files to be copied to destination. Times out after 1 sec.
|
||||
*/
|
||||
public void waitForChanges(int count) throws Exception {
|
||||
// Wait for no more than 1 second by default.
|
||||
waitForChanges(count, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for files to be copied to destination.
|
||||
*
|
||||
* @param count Number of files to wait for.
|
||||
* @param timeOut Timeout in ms. TimeoutException will be thrown if this function times out.
|
||||
*/
|
||||
public void waitForChanges(int count, int timeOut) throws Exception {
|
||||
mNotificationSignal = new CountDownLatch(count);
|
||||
// Signal that the test is now waiting for files.
|
||||
mReadySignal.countDown();
|
||||
if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) {
|
||||
throw new TimeoutException("Timed out waiting for file operations to complete.");
|
||||
void assertAllJobsStarted() {
|
||||
for (TestJob job : jobs) {
|
||||
job.assertStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
|
||||
// Wait until the test is ready to receive file notifications.
|
||||
try {
|
||||
mReadySignal.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "Interrupted while waiting for file copy readiness");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if (DocumentsContract.isDocumentUri(mContext, uri)) {
|
||||
Log.d(TAG, "Notification: " + uri);
|
||||
// Watch for document URI change notifications - this signifies the end of a copy.
|
||||
mNotificationSignal.countDown();
|
||||
}
|
||||
Job createCopy(Context service, Context appContext, Listener listener, String id,
|
||||
DocumentStack stack, List<DocumentInfo> srcs) {
|
||||
TestJob job = new TestJob(service, appContext, listener, OPERATION_COPY, id, stack);
|
||||
jobs.add(job);
|
||||
return job;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
import com.android.documentsui.model.DocumentStack;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@MediumTest
|
||||
public class MoveJobTest extends BaseCopyJobTest {
|
||||
|
||||
public void testMoveFiles() throws Exception {
|
||||
runCopyFilesTest();
|
||||
|
||||
mDocs.assertChildCount(mSrcRoot, 0);
|
||||
}
|
||||
|
||||
public void testMoveVirtualTypedFile() throws Exception {
|
||||
runCopyVirtualTypedFileTest();
|
||||
|
||||
mDocs.assertChildCount(mSrcRoot, 0);
|
||||
}
|
||||
|
||||
public void testMoveVirtualNonTypedFile() throws Exception {
|
||||
runCopyVirtualNonTypedFileTest();
|
||||
|
||||
// should have failed, source not deleted
|
||||
mDocs.assertChildCount(mSrcRoot, 1);
|
||||
}
|
||||
|
||||
public void testMoveEmptyDir() throws Exception {
|
||||
runCopyEmptyDirTest();
|
||||
|
||||
mDocs.assertChildCount(mSrcRoot, 0);
|
||||
}
|
||||
|
||||
public void testMoveDirRecursively() throws Exception {
|
||||
runCopyDirRecursivelyTest();
|
||||
|
||||
mDocs.assertChildCount(mSrcRoot, 0);
|
||||
}
|
||||
|
||||
public void testNoMoveDirToSelf() throws Exception {
|
||||
runNoCopyDirToSelfTest();
|
||||
|
||||
// should have failed, source not deleted
|
||||
mDocs.assertChildCount(mSrcRoot, 1);
|
||||
}
|
||||
|
||||
public void testNoMoveDirToDescendent() throws Exception {
|
||||
runNoCopyDirToDescendentTest();
|
||||
|
||||
// should have failed, source not deleted
|
||||
mDocs.assertChildCount(mSrcRoot, 1);
|
||||
}
|
||||
|
||||
public void testMoveFileWithReadErrors() throws Exception {
|
||||
runCopyFileWithReadErrorsTest();
|
||||
|
||||
// should have failed, source not deleted
|
||||
mDocs.assertChildCount(mSrcRoot, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
CopyJob createJob(List<Uri> srcs, Uri destination) throws Exception {
|
||||
DocumentStack stack = new DocumentStack();
|
||||
stack.push(DocumentInfo.fromUri(mResolver, destination));
|
||||
|
||||
List<DocumentInfo> srcDocs = Lists.newArrayList();
|
||||
for (Uri src : srcs) {
|
||||
srcDocs.add(DocumentInfo.fromUri(mResolver, src));
|
||||
}
|
||||
|
||||
return new MoveJob(
|
||||
mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcDocs);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TestJobListener implements Job.Listener {
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private final List<Job> progress = new ArrayList<>();
|
||||
@Nullable private Job started;
|
||||
@Nullable private Job failed;
|
||||
@Nullable private Job finished;
|
||||
|
||||
@Override
|
||||
public void onStart(Job job) {
|
||||
started = job;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Job job) {
|
||||
failed = job;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Job job) {
|
||||
this.finished = job;
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(CopyJob job) {
|
||||
progress.add(job);
|
||||
}
|
||||
|
||||
public void assertStarted() {
|
||||
if (started == null) {
|
||||
fail("Job didn't start. onStart never called.");
|
||||
}
|
||||
}
|
||||
|
||||
public void assertFinished() {
|
||||
if (finished == null) {
|
||||
fail("Job didn't finish. onFinish never called.");
|
||||
}
|
||||
}
|
||||
|
||||
public void assertFailed() {
|
||||
if (failed == null) {
|
||||
fail("Job didn't fail. onFailed never called.");
|
||||
}
|
||||
}
|
||||
|
||||
public void assertFilesFailed(ArrayList<String> names) {
|
||||
if (failed == null) {
|
||||
fail("Can't test failed documetns. Job didn't fail.");
|
||||
}
|
||||
|
||||
assertEquals(failed.failedFiles.size(), names.size());
|
||||
for (String name : names) {
|
||||
assertFileFailed(name);
|
||||
}
|
||||
}
|
||||
|
||||
public void assertFileFailed(String name) {
|
||||
if (failed == null) {
|
||||
fail("Can't test failed documetns. Job didn't fail.");
|
||||
}
|
||||
|
||||
for (DocumentInfo failed : failed.failedFiles) {
|
||||
if (name.equals(failed.displayName)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Couldn't find failed file: " + name);
|
||||
}
|
||||
|
||||
public void assertCanceled() {
|
||||
if (finished == null) {
|
||||
fail("Can't determine if job was canceled. Job didn't finish.");
|
||||
}
|
||||
if (!finished.isCanceled()) {
|
||||
fail("Job wasn't canceled. Job#isCanceled returned false.");
|
||||
}
|
||||
}
|
||||
|
||||
public void assertMadeProgress() {
|
||||
if (progress.isEmpty()) {
|
||||
fail("Job made no progress. onProgress never called.");
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForFinished() throws InterruptedException {
|
||||
latch.await(500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.documentsui.services;
|
||||
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.fail;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class TestScheduledExecutorService implements ScheduledExecutorService {
|
||||
|
||||
private List<TestFuture> scheduled = new ArrayList<>();
|
||||
private boolean shutdown;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
this.shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
this.shutdown = true;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
void assertShutdown() {
|
||||
if (!shutdown) {
|
||||
fail("Executor wasn't shut down.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Future<T> submit(Callable<T> task) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Future<T> submit(Runnable task, T result) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<?> submit(Runnable task) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
|
||||
throws InterruptedException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout,
|
||||
TimeUnit unit) throws InterruptedException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
|
||||
throws InterruptedException, ExecutionException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
|
||||
TestFuture future = new TestFuture(command, delay, unit);
|
||||
scheduled.add(future);
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,
|
||||
TimeUnit unit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
|
||||
long delay, TimeUnit unit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
void runAll() {
|
||||
for (TestFuture future : scheduled) {
|
||||
future.runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
void run(int taskIndex) {
|
||||
scheduled.get(taskIndex).runnable.run();
|
||||
}
|
||||
|
||||
public void isAlive() {
|
||||
assertFalse(isShutdown());
|
||||
}
|
||||
|
||||
static class TestFuture implements ScheduledFuture<Void> {
|
||||
|
||||
final Runnable runnable;
|
||||
final long delay;
|
||||
final TimeUnit unit;
|
||||
|
||||
public TestFuture(Runnable runnable, long delay, TimeUnit unit) {
|
||||
this.runnable = runnable;
|
||||
this.delay = delay;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed arg0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void get() throws InterruptedException, ExecutionException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void get(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user