Poll jobs' status to update notifications.

Bug: 27249491
Change-Id: I8912c781582af1789c8f76dea06879a3dde75d34
This commit is contained in:
Garfield, Tan
2016-06-09 12:04:22 -07:00
committed by Garfield Tan
parent 9c5a79a389
commit 4a7aba23be
10 changed files with 331 additions and 84 deletions

View File

@@ -157,6 +157,8 @@
<string name="move_preparing">Preparing for move\u2026</string>
<!-- Text shown on the notification while DocumentsUI performs setup in preparation for deleting files [CHAR LIMIT=32] -->
<string name="delete_preparing">Preparing for delete\u2026</string>
<!-- Text progress shown on the notification while DocumentsUI is deleting files. -->
<string name="delete_progress"><xliff:g id="count" example="3">%1$d</xliff:g> / <xliff:g id="totalCount" example="5">%2$d</xliff:g></string>
<!-- Title of the copy error notification [CHAR LIMIT=48] -->
<plurals name="copy_error_notification_title">
<item quantity="one">Couldn\u2019t copy <xliff:g id="count" example="1">%1$d</xliff:g> file</item>

View File

@@ -21,6 +21,7 @@ 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.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
@@ -45,8 +46,6 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.system.ErrnoException;
import android.system.Os;
import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -62,7 +61,6 @@ import libcore.io.IoUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
@@ -70,7 +68,6 @@ import java.util.List;
class CopyJob extends Job {
private static final String TAG = "CopyJob";
private static final int PROGRESS_INTERVAL_MILLIS = 500;
final List<DocumentInfo> mSrcs;
final ArrayList<DocumentInfo> convertedFiles = new ArrayList<>();
@@ -78,8 +75,7 @@ class CopyJob extends Job {
private long mStartTime = -1;
private long mBatchSize;
private long mBytesCopied;
private long mLastNotificationTime;
private volatile long mBytesCopied;
// Speed estimation
private long mBytesCopiedSample;
private long mSampleTime;
@@ -127,16 +123,13 @@ class CopyJob extends Job {
return getSetupNotification(service.getString(R.string.copy_preparing));
}
public boolean shouldUpdateProgress() {
// Wait a while between updates :)
return elapsedRealtime() - mLastNotificationTime > PROGRESS_INTERVAL_MILLIS;
}
Notification getProgressNotification(@StringRes int msgId) {
updateRemainingTimeEstimate();
if (mBatchSize >= 0) {
double completed = (double) this.mBytesCopied / mBatchSize;
mProgressBuilder.setProgress(100, (int) (completed * 100), false);
mProgressBuilder.setContentInfo(
mProgressBuilder.setSubText(
NumberFormat.getPercentInstance().format(completed));
} else {
// If the total file size failed to compute on some files, then show
@@ -153,12 +146,10 @@ class CopyJob extends Job {
mProgressBuilder.setContentText(null);
}
// Remember when we last returned progress so we can provide an answer
// in shouldUpdateProgress.
mLastNotificationTime = elapsedRealtime();
return mProgressBuilder.build();
}
@Override
public Notification getProgressNotification() {
return getProgressNotification(R.string.copy_remaining);
}
@@ -170,11 +161,14 @@ class CopyJob extends Job {
/**
* Generates an estimate of the remaining time in the copy.
*/
void updateRemainingTimeEstimate() {
private void updateRemainingTimeEstimate() {
long elapsedTime = elapsedRealtime() - mStartTime;
// mBytesCopied is modified in worker thread, but this method is called in monitor thread,
// so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
final long bytesCopied = mBytesCopied;
final long sampleDuration = elapsedTime - mSampleTime;
final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
if (mSpeed == 0) {
mSpeed = sampleSpeed;
} else {
@@ -182,13 +176,13 @@ class CopyJob extends Job {
}
if (mSampleTime > 0 && mSpeed > 0) {
mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed;
mRemainingTime = ((mBatchSize - bytesCopied) * 1000) / mSpeed;
} else {
mRemainingTime = 0;
}
mSampleTime = elapsedTime;
mBytesCopiedSample = mBytesCopied;
mBytesCopiedSample = bytesCopied;
}
@Override
@@ -273,10 +267,6 @@ class CopyJob extends Job {
*/
private void makeCopyProgress(long bytesCopied) {
onBytesCopied(bytesCopied);
if (shouldUpdateProgress()) {
updateRemainingTimeEstimate();
listener.onProgress(this);
}
}
/**
@@ -308,6 +298,7 @@ class CopyJob extends Job {
Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
+ " due to an exception: " + e);
}
// If optimized copy fails, then fallback to byte-by-byte copy.
if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
}
@@ -418,14 +409,16 @@ class CopyJob extends Job {
src = DocumentInfo.fromCursor(cursor, srcDir.authority);
processDocument(src, srcDir, destDir);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to recursively process a file %s due to an exception."
.format(srcDir.derivedUri.toString()), e);
Log.e(TAG, String.format(
"Failed to recursively process a file %s due to an exception.",
srcDir.derivedUri.toString()), e);
success = false;
}
}
} catch (RuntimeException e) {
Log.e(TAG, "Failed to copy a file %s to %s. "
.format(srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
Log.e(TAG, String.format(
"Failed to copy a file %s to %s. ",
srcDir.derivedUri.toString(), destDir.derivedUri.toString()), e);
success = false;
} finally {
IoUtils.closeQuietly(cursor);

View File

@@ -37,6 +37,8 @@ final class DeleteJob extends Job {
private List<DocumentInfo> mSrcs;
final DocumentInfo mSrcParent;
private volatile int mDocsProcessed = 0;
/**
* Moves files to a destination identified by {@code destination}.
* Performs most work by delegating to CopyJob, then deleting
@@ -68,6 +70,17 @@ final class DeleteJob extends Job {
return getSetupNotification(service.getString(R.string.delete_preparing));
}
@Override
public Notification getProgressNotification() {
mProgressBuilder.setProgress(mSrcs.size(), mDocsProcessed, false);
String format = service.getString(R.string.delete_progress);
mProgressBuilder.setSubText(String.format(format, mDocsProcessed, mSrcs.size()));
mProgressBuilder.setContentText(null);
return mProgressBuilder.build();
}
@Override
Notification getFailureNotification() {
return getFailureNotification(
@@ -85,10 +98,17 @@ final class DeleteJob extends Job {
if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
try {
deleteDocument(doc, mSrcParent);
if (isCanceled()) {
// Canceled, dump the rest of the work. Deleted docs are not recoverable.
return;
}
} catch (ResourceException e) {
Log.e(TAG, "Failed to delete document @ " + doc.derivedUri);
onFileFailed(doc);
}
++mDocsProcessed;
}
Metrics.logFileOperation(service, operationType, mSrcs, null);
}

View File

@@ -22,6 +22,7 @@ import android.annotation.IntDef;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.Nullable;
@@ -35,6 +36,7 @@ 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;
@@ -85,10 +87,13 @@ public class FileOperationService extends Service implements Job.Listener {
// a sub-optimal arrangement.
@VisibleForTesting ExecutorService executor;
// Use a separate thread pool to prioritize deletions
// Use a separate thread pool to prioritize deletions.
@VisibleForTesting ExecutorService deletionExecutor;
@VisibleForTesting Factory jobFactory;
// Use a handler to schedule monitor tasks.
@VisibleForTesting Handler handler;
private PowerManager mPowerManager;
private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
private NotificationManager mNotificationManager;
@@ -113,6 +118,11 @@ public class FileOperationService extends Service implements Job.Listener {
jobFactory = Job.Factory.instance;
}
if (handler == null) {
// Monitor tasks are small enough to schedule them on main thread.
handler = new Handler();
}
if (DEBUG) Log.d(TAG, "Created.");
mPowerManager = getSystemService(PowerManager.class);
mNotificationManager = getSystemService(NotificationManager.class);
@@ -121,11 +131,20 @@ public class FileOperationService extends Service implements Job.Listener {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "Shutting down executor.");
List<Runnable> unfinished = executor.shutdownNow();
List<Runnable> unfinishedCopies = executor.shutdownNow();
List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow();
List<Runnable> unfinished =
new ArrayList<>(unfinishedCopies.size() + unfinishedDeletions.size());
unfinished.addAll(unfinishedCopies);
unfinished.addAll(unfinishedDeletions);
if (!unfinished.isEmpty()) {
Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
}
executor = null;
deletionExecutor = null;
handler = null;
if (DEBUG) Log.d(TAG, "Destroyed.");
}
@@ -154,7 +173,6 @@ public class FileOperationService extends Service implements Job.Listener {
// Track the service supplied id so we can stop the service once we're out of work to do.
mLastServiceId = serviceId;
Job job = null;
synchronized (mRunning) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
@@ -164,7 +182,7 @@ public class FileOperationService extends Service implements Job.Listener {
DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
job = createJob(operationType, jobId, srcs, srcParent, stack);
Job job = createJob(operationType, jobId, srcs, srcParent, stack);
if (job == null) {
return;
@@ -301,40 +319,45 @@ public class FileOperationService extends Service implements Job.Listener {
@Override
public void onStart(Job job) {
if (DEBUG) Log.d(TAG, "onStart: " + job.id);
mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
// Show start up notification
mNotificationManager.notify(
job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
// Set up related monitor
JobMonitor monitor = new JobMonitor(job, mNotificationManager, handler);
monitor.start();
}
@Override
public void onFinished(Job job) {
assert(job.isFinished());
if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
// Dismiss the ongoing copy notification when the copy is done.
mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
// Use the same thread of monitors to tackle notifications to avoid race conditions.
// Otherwise we may fail to dismiss progress notification.
handler.post(() -> {
// Dismiss the ongoing copy notification when the copy is done.
mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
if (job.hasFailures()) {
Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
}
if (job.hasFailures()) {
Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
}
if (job.hasWarnings()) {
if (DEBUG) Log.d(TAG, "Job finished with warnings.");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
}
if (job.hasWarnings()) {
if (DEBUG) Log.d(TAG, "Job finished with warnings.");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
}
});
synchronized (mRunning) {
deleteJob(job);
}
}
@Override
public void onProgress(CopyJob job) {
if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
mNotificationManager.notify(
job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
}
private static final class JobRecord {
private final Job job;
private final Future<?> future;
@@ -345,6 +368,47 @@ public class FileOperationService extends Service implements Job.Listener {
}
}
/**
* A class used to periodically polls state of a job.
*
* <p>It's possible that jobs hang because underlying document providers stop responding. We
* still need to update notifications if jobs hang, so instead of jobs pushing their states,
* we poll states of jobs.
*/
private static final class JobMonitor implements Runnable {
private static final long INITIAL_PROGRESS_DELAY_MILLIS = 10L;
private static final long PROGRESS_INTERVAL_MILLIS = 500L;
private final Job mJob;
private final NotificationManager mNotificationManager;
private final Handler mHandler;
private JobMonitor(Job job, NotificationManager notificationManager, Handler handler) {
mJob = job;
mNotificationManager = notificationManager;
mHandler = handler;
}
private void start() {
// Delay the first update to avoid dividing by 0 when calculate speed
mHandler.postDelayed(this, INITIAL_PROGRESS_DELAY_MILLIS);
}
@Override
public void run() {
if (mJob.isFinished()) {
// Finish notification is already shown. Progress notification is removed.
// Just finish itself.
return;
}
mNotificationManager.notify(
mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
}
}
@Override
public IBinder onBind(Intent intent) {
return null; // Boilerplate. See super#onBind

View File

@@ -25,6 +25,7 @@ import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LI
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
import android.annotation.DrawableRes;
import android.annotation.IntDef;
import android.annotation.PluralsRes;
import android.app.Notification;
import android.app.Notification.Builder;
@@ -48,6 +49,8 @@ import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -60,6 +63,18 @@ import java.util.Map;
abstract public class Job implements Runnable {
private static final String TAG = "Job";
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CREATED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED})
@interface State {}
static final int STATE_CREATED = 0;
static final int STATE_STARTED = 1;
static final int STATE_COMPLETED = 2;
/**
* A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
* completed.
*/
static final int STATE_CANCELED = 3;
static final String INTENT_TAG_WARNING = "warning";
static final String INTENT_TAG_FAILURE = "failure";
static final String INTENT_TAG_PROGRESS = "progress";
@@ -77,7 +92,7 @@ abstract public class Job implements Runnable {
final Notification.Builder mProgressBuilder;
private final Map<String, ContentProviderClient> mClients = new HashMap<>();
private volatile boolean mCanceled;
private volatile @State int mState = STATE_CREATED;
/**
* A simple progressable job, much like an AsyncTask, but with support
@@ -111,6 +126,12 @@ abstract public class Job implements Runnable {
@Override
public final void run() {
if (isCanceled()) {
// Canceled before running
return;
}
mState = STATE_STARTED;
listener.onStart(this);
try {
start();
@@ -120,6 +141,7 @@ abstract public class Job implements Runnable {
Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Metrics.logFileOperationErrors(service, operationType, failedFiles);
} finally {
mState = (mState == STATE_STARTED) ? STATE_COMPLETED : mState;
listener.onFinished(this);
}
}
@@ -127,8 +149,7 @@ abstract public class Job implements Runnable {
abstract void start();
abstract Notification getSetupNotification();
// TODO: Progress notification for deletes.
// abstract Notification getProgressNotification(long bytesCopied);
abstract Notification getProgressNotification();
abstract Notification getFailureNotification();
abstract Notification getWarningNotification();
@@ -158,13 +179,21 @@ abstract public class Job implements Runnable {
}
}
final @State int getState() {
return mState;
}
final void cancel() {
mCanceled = true;
mState = STATE_CANCELED;
Metrics.logFileOperationCancelled(service, operationType);
}
final boolean isCanceled() {
return mCanceled;
return mState == STATE_CANCELED;
}
final boolean isFinished() {
return mState == STATE_CANCELED || mState == STATE_COMPLETED;
}
final ContentResolver getContentResolver() {
@@ -321,6 +350,5 @@ abstract public class Job implements Runnable {
interface Listener {
void onStart(Job job);
void onFinished(Job job);
void onProgress(CopyJob job);
}
}

View File

@@ -20,6 +20,7 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_CO
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
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;
@@ -31,13 +32,11 @@ import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.Job.Listener;
import com.android.documentsui.testing.TestHandler;
import java.util.ArrayList;
import java.util.List;
/**
* TODO: Test progress updates.
*/
@MediumTest
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {
@@ -49,6 +48,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
private FileOperationService mService;
private TestScheduledExecutorService mExecutor;
private TestScheduledExecutorService mDeletionExecutor;
private TestHandler mHandler;
private TestJobFactory mJobFactory;
public FileOperationServiceTest() {
@@ -62,6 +62,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
mExecutor = new TestScheduledExecutorService();
mDeletionExecutor = new TestScheduledExecutorService();
mHandler = new TestHandler();
mJobFactory = new TestJobFactory();
// Install test doubles.
@@ -73,6 +74,9 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
assertNull(mService.deletionExecutor);
mService.deletionExecutor = mDeletionExecutor;
assertNull(mService.handler);
mService.handler = mHandler;
assertNull(mService.jobFactory);
mService.jobFactory = mJobFactory;
}
@@ -128,6 +132,29 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
mJobFactory.assertNoCopyJobsStarted();
}
public void testUpdatesNotification() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
mExecutor.runAll();
// Assert monitoring continues until job is done
assertTrue(mHandler.hasScheduledMessage());
// Two notifications -- one for setup; one for progress
assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications());
}
public void testStopsUpdatingNotificationAfterFinished() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
mExecutor.runAll();
mHandler.dispatchNextMessage();
// Assert monitoring stops once job is completed.
assertFalse(mHandler.hasScheduledMessage());
// Assert no more notification is generated after finish.
assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications());
}
public void testHoldsWakeLockWhileWorking() throws Exception {
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
@@ -154,11 +181,12 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
mExecutor.assertAlive();
mDeletionExecutor.assertAlive();
mExecutor.runAll();
shutdownService();
mExecutor.assertShutdown();
assertExecutorsShutdown();
}
public void testShutdownStopsExecutor_AfterMixedFailures() throws Exception {
@@ -170,7 +198,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
mExecutor.runAll();
shutdownService();
mExecutor.assertShutdown();
assertExecutorsShutdown();
}
public void testShutdownStopsExecutor_AfterTotalFailure() throws Exception {
@@ -183,7 +211,7 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
mExecutor.runAll();
shutdownService();
mExecutor.assertShutdown();
assertExecutorsShutdown();
}
private Intent createCopyIntent(ArrayList<DocumentInfo> files, DocumentInfo dest)
@@ -217,10 +245,21 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
return destDoc;
}
private void assertExecutorsShutdown() {
mExecutor.assertShutdown();
mDeletionExecutor.assertShutdown();
}
private final class TestJobFactory extends Job.Factory {
final List<TestJob> copyJobs = new ArrayList<>();
final List<TestJob> deleteJobs = new ArrayList<>();
private final List<TestJob> copyJobs = new ArrayList<>();
private final List<TestJob> deleteJobs = new ArrayList<>();
private Runnable mJobRunnable = () -> {
// The following statement is executed concurrently to Job.start() in real situation.
// Call it in TestJob.start() to mimic this behavior.
mHandler.dispatchNextMessage();
};
void assertAllCopyJobsStarted() {
for (TestJob job : copyJobs) {
@@ -258,7 +297,8 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(service, appContext, listener, OPERATION_COPY, id, stack);
TestJob job = new TestJob(
service, appContext, listener, OPERATION_COPY, id, stack, mJobRunnable);
copyJobs.add(job);
return job;
}
@@ -271,7 +311,8 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(service, appContext, listener, OPERATION_DELETE, id, stack);
TestJob job = new TestJob(
service, appContext, listener, OPERATION_DELETE, id, stack, mJobRunnable);
deleteJobs.add(job);
return job;

View File

@@ -22,6 +22,7 @@ import static junit.framework.Assert.assertTrue;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
import android.icu.text.NumberFormat;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
@@ -30,16 +31,23 @@ import com.android.documentsui.model.DocumentStack;
public class TestJob extends Job {
private boolean mStarted;
private Runnable mStartRunnable;
private int mNumOfNotifications = 0;
TestJob(
Context service, Context appContext, Listener listener,
int operationType, String id, DocumentStack stack) {
int operationType, String id, DocumentStack stack, Runnable startRunnable) {
super(service, appContext, listener, operationType, id, stack);
mStartRunnable = startRunnable;
}
@Override
void start() {
mStarted = true;
mStartRunnable.run();
}
void assertStarted() {
@@ -54,11 +62,26 @@ public class TestJob extends Job {
onFileFailed(doc);
}
int getNumOfNotifications() {
return mNumOfNotifications;
}
@Override
Notification getSetupNotification() {
++mNumOfNotifications;
return getSetupNotification(service.getString(R.string.copy_preparing));
}
@Override
Notification getProgressNotification() {
++mNumOfNotifications;
double completed = mStarted ? 1F : 0F;
return mProgressBuilder
.setProgress(1, (int) completed, true)
.setSubText(NumberFormat.getPercentInstance().format(completed))
.build();
}
@Override
Notification getFailureNotification() {
// the "copy" stuff was just convenient and available :)
@@ -73,6 +96,7 @@ public class TestJob extends Job {
@Override
Builder createProgressBuilder() {
++mNumOfNotifications;
// the "copy" stuff was just convenient and available :)
return super.createProgressBuilder(
service.getString(R.string.copy_notification_title),

View File

@@ -46,11 +46,6 @@ public class TestJobListener implements Job.Listener {
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.");

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.testing;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import java.util.TimerTask;
/**
* A test double of {@link Handler}, backed by {@link TestTimer}.
*/
public class TestHandler extends Handler {
private TestTimer mTimer = new TestTimer();
public TestHandler() {
// Use main looper to trick underlying handler, we're not using it at all.
super(Looper.getMainLooper());
}
public boolean hasScheduledMessage() {
return mTimer.hasScheduledTask();
}
public void dispatchNextMessage() {
mTimer.fastForwardToNextTask();
}
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
msg.setTarget(this);
TimerTask task = new MessageTimerTask(msg);
mTimer.scheduleAtTime(new TestTimer.Task(task), uptimeMillis);
return true;
}
private static class MessageTimerTask extends TimerTask {
private Message mMessage;
private MessageTimerTask(Message message) {
mMessage = message;
}
@Override
public void run() {
mMessage.getTarget().dispatchMessage(mMessage);
}
}
}

View File

@@ -47,6 +47,17 @@ public class TestTimer extends Timer {
}
}
public boolean hasScheduledTask() {
return !mTaskList.isEmpty();
}
public void fastForwardToNextTask() {
if (!hasScheduledTask()) {
throw new IllegalStateException("There is no scheduled task!");
}
fastForwardTo(mTaskList.getFirst().mExecuteTime);
}
@Override
public void cancel() {
mTaskList.clear();
@@ -68,7 +79,8 @@ public class TestTimer extends Timer {
@Override
public void schedule(TimerTask task, Date time) {
throw new UnsupportedOperationException();
long executeTime = time.getTime();
scheduleAtTime(task, executeTime);
}
@Override
@@ -79,16 +91,7 @@ public class TestTimer extends Timer {
@Override
public void schedule(TimerTask task, long delay) {
long executeTime = mNow + delay;
Task testTimerTask = (Task) task;
testTimerTask.mExecuteTime = executeTime;
ListIterator<Task> iter = mTaskList.listIterator(0);
while (iter.hasNext()) {
if (iter.next().mExecuteTime >= executeTime) {
break;
}
}
iter.add(testTimerTask);
scheduleAtTime(task, executeTime);
}
@Override
@@ -106,6 +109,19 @@ public class TestTimer extends Timer {
throw new UnsupportedOperationException();
}
public void scheduleAtTime(TimerTask task, long executeTime) {
Task testTimerTask = (Task) task;
testTimerTask.mExecuteTime = executeTime;
ListIterator<Task> iter = mTaskList.listIterator(0);
while (iter.hasNext()) {
if (iter.next().mExecuteTime >= executeTime) {
break;
}
}
iter.add(testTimerTask);
}
public static class Task extends TimerTask {
private boolean mIsCancelled;
private long mExecuteTime;