Some more cleanup and code rearrangement & added unit tests
for ServiceCallback. Change-Id: I6017e50455df3dda9ef6ec54ffd4fbc4e9b1a2d9
This commit is contained in:
parent
3355e58f56
commit
39e3bd63b5
@ -106,7 +106,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
|
||||
// This callback is invoked by the various service implementations to give us download progress
|
||||
// since those modules are responsible for the actual download.
|
||||
private final ServiceCallback mServiceCallback = new ServiceCallback();
|
||||
final ServiceCallback mServiceCallback = new ServiceCallback();
|
||||
|
||||
// sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
|
||||
// by the use of "volatile"
|
||||
@ -115,8 +115,10 @@ public class AttachmentService extends Service implements Runnable {
|
||||
// Signify that we are being shut down & destroyed.
|
||||
private volatile boolean mStop = false;
|
||||
|
||||
Context mContext;
|
||||
EmailConnectivityManager mConnectivityManager;
|
||||
|
||||
// Helper class that keeps track of in progress downloads to make sure that they
|
||||
// are progressing well.
|
||||
final AttachmentWatchdog mWatchdog = new AttachmentWatchdog();
|
||||
|
||||
private final Object mLock = new Object();
|
||||
@ -138,6 +140,16 @@ public class AttachmentService extends Service implements Runnable {
|
||||
|
||||
final DownloadQueue mDownloadQueue = new DownloadQueue();
|
||||
|
||||
// The queue entries here are entries of the form {id, flags}, with the values passed in to
|
||||
// attachmentChanged(). Entries in the queue are picked off by calls to attachmentChanged
|
||||
// and processed in an async task in parallel.
|
||||
private static final Queue<long[]> sAttachmentChangedQueue =
|
||||
new ConcurrentLinkedQueue<long[]>();
|
||||
|
||||
// The task that pulls requests off of the queue of changed attachment and launches
|
||||
// the AttachmentService as needed. Access to this task is guarded by sAttachmentService.
|
||||
private static AsyncTask<Void, Void, Void> sAttachmentChangedTask;
|
||||
|
||||
/**
|
||||
* This class is used to contain the details and state of a particular request to download
|
||||
* an attachment. These objects are constructed and either placed in the {@link DownloadQueue}
|
||||
@ -185,7 +197,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
} else {
|
||||
mAccountId = mMessageId = -1;
|
||||
}
|
||||
mPriority = getPriority(attachment);
|
||||
mPriority = getAttachmentPriority(attachment);
|
||||
mTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
@ -399,6 +411,10 @@ public class AttachmentService extends Service implements Runnable {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// TODO: Really don't like hard coding the AttachmentService reference here
|
||||
// as it makes testing harder if we are trying to mock out the service
|
||||
// We should change this with some sort of getter that returns the
|
||||
// static (or test) AttachmentService instance to use.
|
||||
final AttachmentService service = AttachmentService.sRunningService;
|
||||
if (service != null) {
|
||||
// If our service instance is gone, just leave
|
||||
@ -412,22 +428,32 @@ public class AttachmentService extends Service implements Runnable {
|
||||
}, "AttachmentService AttachmentWatchdog").start();
|
||||
}
|
||||
|
||||
boolean validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout,
|
||||
final long now) {
|
||||
// Check how long it's been since receiving a callback
|
||||
final long timeSinceCallback = now - dr.mLastCallbackTime;
|
||||
if (timeSinceCallback > callbackTimeout) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "== Download of " + dr.mAttachmentId + " timed out");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watchdog for downloads; we use this in case we are hanging on a download, which might
|
||||
* have failed silently (the connection dropped, for example)
|
||||
*/
|
||||
void watchdogAlarm(final AttachmentService service, final int callbackTimeout) {
|
||||
final long now = System.currentTimeMillis();
|
||||
// We want to iterate on each of the downloads that are currently in progress and
|
||||
// cancel the ones that seem to be taking too long.
|
||||
final Collection<DownloadRequest> inProgressRequests = service.getInProgressDownloads();
|
||||
final Collection<DownloadRequest> inProgressRequests =
|
||||
service.mDownloadsInProgress.values();
|
||||
for (DownloadRequest req: inProgressRequests) {
|
||||
// Check how long it's been since receiving a callback
|
||||
final long timeSinceCallback = now - req.mLastCallbackTime;
|
||||
if (timeSinceCallback > callbackTimeout) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "== Download of " + req.mAttachmentId + " timed out");
|
||||
}
|
||||
final boolean shouldCancelDownload = validateDownloadRequest(req, callbackTimeout,
|
||||
System.currentTimeMillis());
|
||||
if (shouldCancelDownload) {
|
||||
service.cancelDownload(req);
|
||||
// TODO: Should we also mark the attachment as failed at this point in time?
|
||||
}
|
||||
@ -436,7 +462,11 @@ public class AttachmentService extends Service implements Runnable {
|
||||
if (service.isConnected()) {
|
||||
service.processQueue();
|
||||
}
|
||||
if (service.areDownloadsInProgress()) {
|
||||
issueNextWatchdogAlarm(service);
|
||||
}
|
||||
|
||||
void issueNextWatchdogAlarm(final AttachmentService service) {
|
||||
if (!service.mDownloadsInProgress.isEmpty()) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "Reschedule watchdog...");
|
||||
}
|
||||
@ -445,31 +475,257 @@ public class AttachmentService extends Service implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isConnected() {
|
||||
if (mConnectivityManager != null) {
|
||||
return mConnectivityManager.hasConnectivity();
|
||||
/**
|
||||
* We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks
|
||||
* come from either Controller (IMAP/POP) or ExchangeService (EAS). Note that we only implement the
|
||||
* single callback that's defined by the EmailServiceCallback interface.
|
||||
*/
|
||||
class ServiceCallback extends IEmailServiceCallback.Stub {
|
||||
|
||||
/**
|
||||
* Simple routine to generate updated status values for the Attachment based on the
|
||||
* service callback. Right now it is very simple but factoring out this code allows us
|
||||
* to test easier and very easy to expand in the future.
|
||||
*/
|
||||
ContentValues getAttachmentUpdateValues(final Attachment attachment,
|
||||
final int statusCode, final int progress) {
|
||||
final ContentValues values = new ContentValues();
|
||||
if (attachment != null) {
|
||||
if (statusCode == EmailServiceStatus.IN_PROGRESS) {
|
||||
// TODO: What else do we want to expose about this in-progress download through
|
||||
// the provider? If there is more, make sure that the service implementation
|
||||
// reports it and make sure that we add it here.
|
||||
values.put(AttachmentColumns.UI_STATE, AttachmentState.DOWNLOADING);
|
||||
values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
|
||||
attachment.mSize * progress / 100);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Collection<DownloadRequest> getInProgressDownloads() {
|
||||
return mDownloadsInProgress.values();
|
||||
}
|
||||
@Override
|
||||
public void loadAttachmentStatus(final long messageId, final long attachmentId,
|
||||
final int statusCode, final int progress) {
|
||||
// Record status and progress
|
||||
final DownloadRequest req = mDownloadsInProgress.get(attachmentId);
|
||||
if (req != null) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
final String code;
|
||||
switch(statusCode) {
|
||||
case EmailServiceStatus.SUCCESS: code = "Success"; break;
|
||||
case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break;
|
||||
default: code = Integer.toString(statusCode); break;
|
||||
}
|
||||
if (statusCode != EmailServiceStatus.IN_PROGRESS) {
|
||||
LogUtils.d(LOG_TAG, ">> Attachment status " + attachmentId + ": " + code);
|
||||
} else if (progress >= (req.mLastProgress + 10)) {
|
||||
LogUtils.d(LOG_TAG, ">> Attachment progress %d: %d%%", attachmentId,
|
||||
progress);
|
||||
}
|
||||
}
|
||||
|
||||
boolean areDownloadsInProgress() {
|
||||
return !mDownloadsInProgress.isEmpty();
|
||||
// Update some state to keep track of the progress of the download
|
||||
req.mLastStatusCode = statusCode;
|
||||
req.mLastProgress = progress;
|
||||
req.mLastCallbackTime = System.currentTimeMillis();
|
||||
|
||||
// Update the attachment status in the provider.
|
||||
final Attachment attachment =
|
||||
Attachment.restoreAttachmentWithId(AttachmentService.this, attachmentId);
|
||||
final ContentValues values = getAttachmentUpdateValues(attachment, statusCode,
|
||||
progress);
|
||||
if (values.size() > 0) {
|
||||
attachment.update(AttachmentService.this, values);
|
||||
}
|
||||
|
||||
switch (statusCode) {
|
||||
case EmailServiceStatus.IN_PROGRESS:
|
||||
break;
|
||||
default:
|
||||
// It is assumed that any other error is either a success or an error
|
||||
// Either way, the final updates to the DownloadRequest and attachment
|
||||
// objects will be handed there.
|
||||
endDownload(attachmentId, statusCode);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// The only way that we can get a callback from the service implementation for
|
||||
// an attachment that doesn't exist is if it was cancelled due to the
|
||||
// AttachmentWatchdog. This is a valid scenario and the Watchdog should have already
|
||||
// marked this attachment as failed/cancelled.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bits in the provider to mark this download as failed.
|
||||
* @param att The attachment that failed to download.
|
||||
* Called directly by EmailProvider whenever an attachment is inserted or changed. Since this
|
||||
* call is being invoked on the UI thread, we need to make sure that the downloads are
|
||||
* happening in the background.
|
||||
* @param context the caller's context
|
||||
* @param id the attachment's id
|
||||
* @param flags the new flags for the attachment
|
||||
*/
|
||||
void markAttachmentAsFailed(final Attachment att) {
|
||||
final ContentValues cv = new ContentValues();
|
||||
final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
|
||||
cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
|
||||
att.update(mContext, cv);
|
||||
public static void attachmentChanged(final Context context, final long id, final int flags) {
|
||||
synchronized (sAttachmentChangedQueue) {
|
||||
sAttachmentChangedQueue.add(new long[]{id, flags});
|
||||
if (sAttachmentChangedTask == null) {
|
||||
sAttachmentChangedTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
while (true) {
|
||||
final long[] change;
|
||||
synchronized (sAttachmentChangedQueue) {
|
||||
change = sAttachmentChangedQueue.poll();
|
||||
if (change == null) {
|
||||
sAttachmentChangedTask = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final long id = change[0];
|
||||
final long flags = change[1];
|
||||
final Attachment attachment =
|
||||
Attachment.restoreAttachmentWithId(context, id);
|
||||
if (attachment == null) {
|
||||
continue;
|
||||
}
|
||||
attachment.mFlags = (int) flags;
|
||||
final Intent intent = new Intent(context, AttachmentService.class);
|
||||
intent.putExtra(EXTRA_ATTACHMENT, attachment);
|
||||
// This is result in a call to AttachmentService.onStartCommand()
|
||||
// which will queue the attachment in its internal prioritized queue.
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point for this service, the attachment to download can be identified
|
||||
* by the EXTRA_ATTACHMENT extra in the intent.
|
||||
*/
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
if (sRunningService == null) {
|
||||
sRunningService = this;
|
||||
}
|
||||
if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
|
||||
Attachment att = intent.getParcelableExtra(EXTRA_ATTACHMENT);
|
||||
onChange(this, att);
|
||||
} else {
|
||||
LogUtils.wtf(LOG_TAG, "Received an invalid intent w/o EXTRA_ATTACHMENT");
|
||||
}
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Most of the leg work is done by our service thread that is created when this
|
||||
* service is created.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Start up our service thread.
|
||||
new Thread(this, "AttachmentService").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Mark this instance of the service as stopped. Our main loop for the AttachmentService
|
||||
// checks for this flag along with the AttachmentWatchdog.
|
||||
mStop = true;
|
||||
if (sRunningService != null) {
|
||||
// Kick it awake to get it to realize that we are stopping.
|
||||
kick();
|
||||
sRunningService = null;
|
||||
}
|
||||
if (mConnectivityManager != null) {
|
||||
mConnectivityManager.unregister();
|
||||
mConnectivityManager.stopWait();
|
||||
mConnectivityManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main routine for our AttachmentService service thread.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
// These fields are only used within the service thread
|
||||
mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG);
|
||||
mAccountManagerStub = new AccountManagerStub(this);
|
||||
|
||||
// Run through all attachments in the database that require download and add them to
|
||||
// the queue. This is the case where a previous AttachmentService may have been notified
|
||||
// to stop before processing everything in its queue.
|
||||
final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
final Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
|
||||
EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
|
||||
new String[] {Integer.toString(mask)}, null);
|
||||
try {
|
||||
LogUtils.d(LOG_TAG, "Count: " + c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
final Attachment attachment = Attachment.restoreAttachmentWithId(
|
||||
this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
|
||||
if (attachment != null) {
|
||||
onChange(this, attachment);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
// Loop until stopped, with a 30 minute wait loop
|
||||
while (!mStop) {
|
||||
// Here's where we run our attachment loading logic...
|
||||
// Make a local copy of the variable so we don't null-crash on service shutdown
|
||||
final EmailConnectivityManager ecm = mConnectivityManager;
|
||||
if (ecm != null) {
|
||||
ecm.waitForConnectivity();
|
||||
}
|
||||
if (mStop) {
|
||||
// We might be bailing out here due to the service shutting down
|
||||
LogUtils.d(LOG_TAG, "*** AttachmentService has been instructed to stop");
|
||||
break;
|
||||
}
|
||||
processQueue();
|
||||
if (mDownloadQueue.isEmpty()) {
|
||||
LogUtils.d(LOG_TAG, "*** All done; shutting down service");
|
||||
stopSelf();
|
||||
break;
|
||||
}
|
||||
synchronized(mLock) {
|
||||
try {
|
||||
mLock.wait(PROCESS_QUEUE_WAIT_TIME);
|
||||
} catch (InterruptedException e) {
|
||||
// That's ok; we'll just keep looping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister now that we're done
|
||||
// Make a local copy of the variable so we don't null-crash on service shutdown
|
||||
final EmailConnectivityManager ecm = mConnectivityManager;
|
||||
if (ecm != null) {
|
||||
ecm.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Function that kicks the service into action as it may be waiting for this object
|
||||
* as it processed the last round of attachments.
|
||||
*/
|
||||
private void kick() {
|
||||
synchronized(mLock) {
|
||||
mLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -480,7 +736,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
*/
|
||||
public synchronized void onChange(final Context context, final Attachment att) {
|
||||
DownloadRequest req = mDownloadQueue.findRequestById(att.mId);
|
||||
final long priority = getPriority(att);
|
||||
final long priority = getAttachmentPriority(att);
|
||||
if (priority == PRIORITY_NONE) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "== Attachment changed: " + att.mId);
|
||||
@ -536,6 +792,18 @@ public class AttachmentService extends Service implements Runnable {
|
||||
kick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bits in the provider to mark this download as failed.
|
||||
* @param att The attachment that failed to download.
|
||||
*/
|
||||
void markAttachmentAsFailed(final Attachment att) {
|
||||
final ContentValues cv = new ContentValues();
|
||||
final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
|
||||
cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
|
||||
att.update(this, cv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
|
||||
* the limit on maximum downloads
|
||||
@ -553,20 +821,20 @@ public class AttachmentService extends Service implements Runnable {
|
||||
break;
|
||||
}
|
||||
// Enforce per-account limit here
|
||||
if (downloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
|
||||
if (getDownloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "== Skip #" + req.mAttachmentId + "; maxed for acct #" +
|
||||
req.mAccountId);
|
||||
}
|
||||
continue;
|
||||
} else if (Attachment.restoreAttachmentWithId(mContext, req.mAttachmentId) == null) {
|
||||
} else if (Attachment.restoreAttachmentWithId(this, req.mAttachmentId) == null) {
|
||||
continue;
|
||||
}
|
||||
if (!req.mInProgress) {
|
||||
final long currentTime = SystemClock.elapsedRealtime();
|
||||
if (req.mRetryCount > 0 && req.mRetryStartTime > currentTime) {
|
||||
LogUtils.d(LOG_TAG, "== waiting to retry attachment %d", req.mAttachmentId);
|
||||
mWatchdog.setWatchdogAlarm(mContext, CONNECTION_ERROR_RETRY_MILLIS,
|
||||
mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
|
||||
CALLBACK_TIMEOUT);
|
||||
continue;
|
||||
}
|
||||
@ -600,25 +868,25 @@ public class AttachmentService extends Service implements Runnable {
|
||||
// backgroundDownloads instead? We should fix and test this.
|
||||
final Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
|
||||
MAX_ATTACHMENTS_TO_CHECK);
|
||||
final Cursor c = mContext.getContentResolver().query(lookupUri,
|
||||
final Cursor c = this.getContentResolver().query(lookupUri,
|
||||
Attachment.CONTENT_PROJECTION,
|
||||
EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
|
||||
null, AttachmentColumns._ID + " DESC");
|
||||
File cacheDir = mContext.getCacheDir();
|
||||
File cacheDir = this.getCacheDir();
|
||||
try {
|
||||
while (c.moveToNext()) {
|
||||
final Attachment att = new Attachment();
|
||||
att.restore(c);
|
||||
final Account account = Account.restoreAccountWithId(mContext, att.mAccountKey);
|
||||
final Account account = Account.restoreAccountWithId(this, att.mAccountKey);
|
||||
if (account == null) {
|
||||
// Clean up this orphaned attachment; there's no point in keeping it
|
||||
// around; then try to find another one
|
||||
EmailContent.delete(mContext, Attachment.CONTENT_URI, att.mId);
|
||||
EmailContent.delete(this, Attachment.CONTENT_URI, att.mId);
|
||||
} else {
|
||||
// Check that the attachment meets system requirements for download
|
||||
// Note that there couple be policy that does not allow this attachment
|
||||
// to be downloaded.
|
||||
final AttachmentInfo info = new AttachmentInfo(mContext, att);
|
||||
final AttachmentInfo info = new AttachmentInfo(this, att);
|
||||
if (info.isEligibleForDownload()) {
|
||||
// Either the account must be able to prefetch or this must be
|
||||
// an inline attachment.
|
||||
@ -630,7 +898,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
continue;
|
||||
}
|
||||
// Start this download and we're done
|
||||
final DownloadRequest req = new DownloadRequest(mContext, att);
|
||||
final DownloadRequest req = new DownloadRequest(this, att);
|
||||
tryStartDownload(req);
|
||||
break;
|
||||
}
|
||||
@ -650,21 +918,6 @@ public class AttachmentService extends Service implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of running downloads in progress for this account
|
||||
* @param accountId the id of the account
|
||||
* @return the count of running downloads
|
||||
*/
|
||||
synchronized int downloadsForAccount(final long accountId) {
|
||||
int count = 0;
|
||||
for (final DownloadRequest req: mDownloadsInProgress.values()) {
|
||||
if (req.mAccountId == accountId) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
|
||||
* parameter
|
||||
@ -707,7 +960,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
mDownloadsInProgress.put(req.mAttachmentId, req);
|
||||
service.loadAttachment(mServiceCallback, req.mAccountId, req.mAttachmentId,
|
||||
req.mPriority != PRIORITY_FOREGROUND);
|
||||
mWatchdog.setWatchdogAlarm(mContext);
|
||||
mWatchdog.setWatchdogAlarm(this);
|
||||
}
|
||||
|
||||
synchronized void cancelDownload(final DownloadRequest req) {
|
||||
@ -778,7 +1031,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
req.mInProgress = false;
|
||||
req.mRetryStartTime = SystemClock.elapsedRealtime() +
|
||||
CONNECTION_ERROR_RETRY_MILLIS;
|
||||
mWatchdog.setWatchdogAlarm(mContext, CONNECTION_ERROR_RETRY_MILLIS,
|
||||
mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
|
||||
CALLBACK_TIMEOUT);
|
||||
} else {
|
||||
LogUtils.d(LOG_TAG, "ConnectionError #%d, retried %d times, adding delay",
|
||||
@ -807,7 +1060,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
+ " seconds from request, status: " + status);
|
||||
}
|
||||
|
||||
final Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
|
||||
final Attachment attachment = Attachment.restoreAttachmentWithId(this, attachmentId);
|
||||
if (attachment != null) {
|
||||
final long accountId = attachment.mAccountKey;
|
||||
// Update our attachment storage for this account
|
||||
@ -822,22 +1075,22 @@ public class AttachmentService extends Service implements Runnable {
|
||||
// If this is a forwarding download, and the attachment doesn't exist (or
|
||||
// can't be downloaded) delete it from the outgoing message, lest that
|
||||
// message never get sent
|
||||
EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
|
||||
EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
|
||||
// TODO: Talk to UX about whether this is even worth doing
|
||||
NotificationController nc = NotificationController.getInstance(mContext);
|
||||
NotificationController nc = NotificationController.getInstance(this);
|
||||
nc.showDownloadForwardFailedNotification(attachment);
|
||||
deleted = true;
|
||||
}
|
||||
// If we're an attachment on forwarded mail, and if we're not still blocked,
|
||||
// try to send pending mail now (as mediated by MailService)
|
||||
if ((req != null) &&
|
||||
!Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) {
|
||||
!Utility.hasUnloadedAttachments(this, attachment.mMessageKey)) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
LogUtils.d(LOG_TAG, "== Downloads finished for outgoing msg #"
|
||||
+ req.mMessageId);
|
||||
}
|
||||
EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
|
||||
mContext, accountId);
|
||||
this, accountId);
|
||||
try {
|
||||
service.sendMail(accountId);
|
||||
} catch (RemoteException e) {
|
||||
@ -846,10 +1099,10 @@ public class AttachmentService extends Service implements Runnable {
|
||||
}
|
||||
}
|
||||
if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
|
||||
Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
|
||||
Message msg = Message.restoreMessageWithId(this, attachment.mMessageKey);
|
||||
if (msg == null) {
|
||||
// If there's no associated message, delete the attachment
|
||||
EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
|
||||
EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
|
||||
} else {
|
||||
// If there really is a message, retry
|
||||
// TODO: How will this get retried? It's still marked as inProgress?
|
||||
@ -867,20 +1120,35 @@ public class AttachmentService extends Service implements Runnable {
|
||||
Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
cv.put(AttachmentColumns.FLAGS, attachment.mFlags &= ~flags);
|
||||
cv.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
|
||||
attachment.update(mContext, cv);
|
||||
attachment.update(this, cv);
|
||||
}
|
||||
}
|
||||
// Process the queue
|
||||
kick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of running downloads in progress for this account
|
||||
* @param accountId the id of the account
|
||||
* @return the count of running downloads
|
||||
*/
|
||||
synchronized int getDownloadsForAccount(final long accountId) {
|
||||
int count = 0;
|
||||
for (final DownloadRequest req: mDownloadsInProgress.values()) {
|
||||
if (req.mAccountId == accountId) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the download priority of an Attachment. A priority of zero means that the
|
||||
* attachment is not marked for download.
|
||||
* @param att the Attachment
|
||||
* @return the priority key of the Attachment
|
||||
*/
|
||||
private static int getPriority(final Attachment att) {
|
||||
private static int getAttachmentPriority(final Attachment att) {
|
||||
int priorityClass = PRIORITY_NONE;
|
||||
final int flags = att.mFlags;
|
||||
if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
||||
@ -891,117 +1159,6 @@ public class AttachmentService extends Service implements Runnable {
|
||||
return priorityClass;
|
||||
}
|
||||
|
||||
private void kick() {
|
||||
synchronized(mLock) {
|
||||
mLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks
|
||||
* come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the
|
||||
* single callback that's defined by the EmailServiceCallback interface.
|
||||
*/
|
||||
class ServiceCallback extends IEmailServiceCallback.Stub {
|
||||
@Override
|
||||
public void loadAttachmentStatus(final long messageId, final long attachmentId,
|
||||
final int statusCode, final int progress) {
|
||||
// Record status and progress
|
||||
final DownloadRequest req = mDownloadsInProgress.get(attachmentId);
|
||||
if (req != null) {
|
||||
if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
|
||||
final String code;
|
||||
switch(statusCode) {
|
||||
case EmailServiceStatus.SUCCESS: code = "Success"; break;
|
||||
case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break;
|
||||
default: code = Integer.toString(statusCode); break;
|
||||
}
|
||||
if (statusCode != EmailServiceStatus.IN_PROGRESS) {
|
||||
LogUtils.d(LOG_TAG, ">> Attachment status " + attachmentId + ": " + code);
|
||||
} else if (progress >= (req.mLastProgress + 10)) {
|
||||
LogUtils.d(LOG_TAG, ">> Attachment progress %d: %d%%", attachmentId, progress);
|
||||
}
|
||||
}
|
||||
req.mLastStatusCode = statusCode;
|
||||
req.mLastProgress = progress;
|
||||
req.mLastCallbackTime = System.currentTimeMillis();
|
||||
final Attachment attachment =
|
||||
Attachment.restoreAttachmentWithId(mContext, attachmentId);
|
||||
if (attachment != null && statusCode == EmailServiceStatus.IN_PROGRESS) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
|
||||
attachment.mSize * progress / 100);
|
||||
// Update UIProvider with updated download size
|
||||
// Individual services will set contentUri and state when finished
|
||||
attachment.update(mContext, values);
|
||||
}
|
||||
switch (statusCode) {
|
||||
case EmailServiceStatus.IN_PROGRESS:
|
||||
break;
|
||||
default:
|
||||
endDownload(attachmentId, statusCode);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// The only way that we can get a callback from the service implementation for
|
||||
// an attachment that doesn't exist is if it was cancelled due to the
|
||||
// AttachmentWatchdog. This is a valid scenario and the Watchdog should have already
|
||||
// marked this attachment as failed/cancelled.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The queue entries here are entries of the form {id, flags}, with the values passed in to
|
||||
// attachmentChanged()
|
||||
private static final Queue<long[]> sAttachmentChangedQueue =
|
||||
new ConcurrentLinkedQueue<long[]>();
|
||||
private static AsyncTask<Void, Void, Void> sAttachmentChangedTask;
|
||||
|
||||
/**
|
||||
* Called directly by EmailProvider whenever an attachment is inserted or changed. Since this
|
||||
* call is being invoked on the UI thread, we need to make sure that the downloads are
|
||||
* happening in the background.
|
||||
* @param context the caller's context
|
||||
* @param id the attachment's id
|
||||
* @param flags the new flags for the attachment
|
||||
*/
|
||||
public static void attachmentChanged(final Context context, final long id, final int flags) {
|
||||
synchronized (sAttachmentChangedQueue) {
|
||||
sAttachmentChangedQueue.add(new long[]{id, flags});
|
||||
if (sAttachmentChangedTask == null) {
|
||||
sAttachmentChangedTask = new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
while (true) {
|
||||
final long[] change;
|
||||
synchronized (sAttachmentChangedQueue) {
|
||||
change = sAttachmentChangedQueue.poll();
|
||||
if (change == null) {
|
||||
sAttachmentChangedTask = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final long id = change[0];
|
||||
final long flags = change[1];
|
||||
final Attachment attachment =
|
||||
Attachment.restoreAttachmentWithId(context, id);
|
||||
if (attachment == null) {
|
||||
continue;
|
||||
}
|
||||
attachment.mFlags = (int) flags;
|
||||
final Intent intent =
|
||||
new Intent(context, AttachmentService.class);
|
||||
intent.putExtra(EXTRA_ATTACHMENT, attachment);
|
||||
// This is result in a call to AttachmentService.onStartCommand()
|
||||
// which will queue the attachment in its internal prioritized queue.
|
||||
context.startService(intent);
|
||||
}
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an attachment can be prefetched for the given account based on
|
||||
* total download size restrictions tied to the account.
|
||||
@ -1026,7 +1183,7 @@ public class AttachmentService extends Service implements Runnable {
|
||||
// do this as you may assign more storage to your corporate account rather than a personal
|
||||
// account.
|
||||
final long perAccountMaxStorage =
|
||||
(long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
|
||||
(long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
|
||||
|
||||
// Retrieve our idea of currently used attachment storage; since we don't track deletions,
|
||||
// this number is the "worst case". If the number is greater than what's allowed per
|
||||
@ -1056,114 +1213,11 @@ public class AttachmentService extends Service implements Runnable {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main routine for our AttachmentService service thread.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
// These fields are only used within the service thread
|
||||
mContext = this;
|
||||
mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG);
|
||||
mAccountManagerStub = new AccountManagerStub(this);
|
||||
|
||||
// Run through all attachments in the database that require download and add them to
|
||||
// the queue. This is the case where a previous AttachmentService may have been notified
|
||||
// to stop before processing everything in its queue.
|
||||
final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
final Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
|
||||
EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
|
||||
new String[] {Integer.toString(mask)}, null);
|
||||
try {
|
||||
LogUtils.d(LOG_TAG, "Count: " + c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
final Attachment attachment = Attachment.restoreAttachmentWithId(
|
||||
this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
|
||||
if (attachment != null) {
|
||||
onChange(this, attachment);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
// Loop until stopped, with a 30 minute wait loop
|
||||
while (!mStop) {
|
||||
// Here's where we run our attachment loading logic...
|
||||
// Make a local copy of the variable so we don't null-crash on service shutdown
|
||||
final EmailConnectivityManager ecm = mConnectivityManager;
|
||||
if (ecm != null) {
|
||||
ecm.waitForConnectivity();
|
||||
}
|
||||
if (mStop) {
|
||||
// We might be bailing out here due to the service shutting down
|
||||
LogUtils.d(LOG_TAG, "*** AttachmentService has been instructed to stop");
|
||||
break;
|
||||
}
|
||||
processQueue();
|
||||
if (mDownloadQueue.isEmpty()) {
|
||||
LogUtils.d(LOG_TAG, "*** All done; shutting down service");
|
||||
stopSelf();
|
||||
break;
|
||||
}
|
||||
synchronized(mLock) {
|
||||
try {
|
||||
mLock.wait(PROCESS_QUEUE_WAIT_TIME);
|
||||
} catch (InterruptedException e) {
|
||||
// That's ok; we'll just keep looping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister now that we're done
|
||||
// Make a local copy of the variable so we don't null-crash on service shutdown
|
||||
final EmailConnectivityManager ecm = mConnectivityManager;
|
||||
if (ecm != null) {
|
||||
ecm.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||
if (sRunningService == null) {
|
||||
sRunningService = this;
|
||||
}
|
||||
if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
|
||||
Attachment att = intent.getParcelableExtra(EXTRA_ATTACHMENT);
|
||||
onChange(mContext, att);
|
||||
} else {
|
||||
LogUtils.wtf(LOG_TAG, "Received an invalid intent w/o EXTRA_ATTACHMENT");
|
||||
}
|
||||
return Service.START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Start up our service thread.
|
||||
new Thread(this, "AttachmentService").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(final Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Mark this instance of the service as stopped. Our main loop for the AttachmentService
|
||||
// checks for this flag along with the AttachmentWatchdog.
|
||||
mStop = true;
|
||||
if (sRunningService != null) {
|
||||
// Kick it awake to get it to realize that we are stopping.
|
||||
kick();
|
||||
sRunningService = null;
|
||||
}
|
||||
boolean isConnected() {
|
||||
if (mConnectivityManager != null) {
|
||||
mConnectivityManager.unregister();
|
||||
mConnectivityManager.stopWait();
|
||||
mConnectivityManager = null;
|
||||
return mConnectivityManager.hasConnectivity();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// For Debugging.
|
||||
|
@ -16,9 +16,14 @@
|
||||
|
||||
package com.android.email.service;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.test.suitebuilder.annotation.SmallTest;
|
||||
|
||||
import com.android.emailcommon.provider.EmailContent;
|
||||
import com.android.emailcommon.service.EmailServiceStatus;
|
||||
import com.android.mail.providers.UIProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
@ -398,27 +403,24 @@ public class AttachmentServiceTests extends TestCase {
|
||||
* whenever the onReceive() call is made by the AlarmManager
|
||||
*/
|
||||
public void testAttachmentWatchdogAlarm() {
|
||||
final MockAttachmentService mockAttachmentService = new MockAttachmentService();
|
||||
final AttachmentService attachmentService = new AttachmentService();
|
||||
final AttachmentService.AttachmentWatchdog watchdog = attachmentService.mWatchdog;
|
||||
|
||||
// Add a couple of items to the in-progress queue
|
||||
final AttachmentService.AttachmentWatchdog testWatchdog =
|
||||
new AttachmentService.AttachmentWatchdog();
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// Add one download request object to the in process map that should
|
||||
// should not need to be cancelled.
|
||||
final AttachmentService.DownloadRequest dr =
|
||||
new AttachmentService.DownloadRequest(AttachmentService.PRIORITY_FOREGROUND, 1);
|
||||
dr.mLastCallbackTime = System.currentTimeMillis();
|
||||
mockAttachmentService.mDownloadsInProgress.put(dr.mAttachmentId, dr);
|
||||
dr.mLastCallbackTime = now;
|
||||
attachmentService.mDownloadsInProgress.put(dr.mAttachmentId, dr);
|
||||
|
||||
// Set the alarm to delay 1 second and too look for attachments that have been
|
||||
// not updated for 60 seconds.
|
||||
testWatchdog.watchdogAlarm(mockAttachmentService, 60000);
|
||||
// Only request the DownloadRequest to cancelled if it is older than 60 seconds,
|
||||
// which is not true in this case.
|
||||
final boolean shouldCancel = watchdog.validateDownloadRequest(dr, 60000, now);
|
||||
|
||||
// Now check the results. The code should have called not cancelled anything but should
|
||||
// have called processQueue()
|
||||
assertTrue(mockAttachmentService.mCalledProcessQueue);
|
||||
assertFalse(mockAttachmentService.mCalledCancel);
|
||||
// Now check the results. We should not be asked to cancel this DownloadRequest
|
||||
assertFalse(shouldCancel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -426,57 +428,70 @@ public class AttachmentServiceTests extends TestCase {
|
||||
* whenever the onReceive() call is made by the AlarmManager
|
||||
*/
|
||||
public void testAttachmentWatchdogAlarmNeedsCancel() {
|
||||
final MockAttachmentService mockAttachmentService = new MockAttachmentService();
|
||||
final AttachmentService attachmentService = new AttachmentService();
|
||||
final AttachmentService.AttachmentWatchdog watchdog = attachmentService.mWatchdog;
|
||||
|
||||
// Add a couple of items to the in-progress queue
|
||||
final AttachmentService.AttachmentWatchdog testWatchdog =
|
||||
new AttachmentService.AttachmentWatchdog();
|
||||
final long now = System.currentTimeMillis();
|
||||
|
||||
// Add one download request object to the in process map that should
|
||||
// be cancelled by the time the callback is executed.
|
||||
// should not need to be cancelled.
|
||||
final AttachmentService.DownloadRequest dr =
|
||||
new AttachmentService.DownloadRequest(AttachmentService.PRIORITY_FOREGROUND, 1);
|
||||
dr.mLastCallbackTime = System.currentTimeMillis() - 60000;
|
||||
mockAttachmentService.mDownloadsInProgress.put(dr.mAttachmentId, dr);
|
||||
dr.mLastCallbackTime = now - 60000; // Set this request to be 60 seconds old.
|
||||
attachmentService.mDownloadsInProgress.put(dr.mAttachmentId, dr);
|
||||
|
||||
// Set the alarm to delay 1 second and too look for attachments that have been
|
||||
// not updated for 10 seconds.
|
||||
// Set the alarm to delay 1 second and too look for attachments that have been
|
||||
// not updated for 60 seconds.
|
||||
testWatchdog.watchdogAlarm(mockAttachmentService, 1000);
|
||||
// Request cancellation for DownloadRequests that are older than a second.
|
||||
// For this test, this is true.
|
||||
final boolean shouldCancel = watchdog.validateDownloadRequest(dr, 1000, now);
|
||||
|
||||
// Now check the results. The code should have called both cancelDownload and
|
||||
// processQueue()
|
||||
assertTrue(mockAttachmentService.mCalledProcessQueue);
|
||||
assertTrue(mockAttachmentService.mCalledCancel);
|
||||
// Now check the results. We should not be asked to cancel this DownloadRequest
|
||||
assertTrue(shouldCancel);
|
||||
}
|
||||
|
||||
// Mock test class to stub out a couple of functions but record that they were called.
|
||||
class MockAttachmentService extends AttachmentService {
|
||||
// For AttachmentWatchdog tests to see if certain functions were called.
|
||||
public boolean mCalledCancel = false;
|
||||
public boolean mCalledProcessQueue = false;
|
||||
public void testServiceCallbackAttachmentCompleteUpdate() {
|
||||
final AttachmentService attachmentService = new AttachmentService();
|
||||
final EmailContent.Attachment attachment = new EmailContent.Attachment();
|
||||
attachment.mSize = 1000;
|
||||
|
||||
public MockAttachmentService() {
|
||||
sRunningService = this;
|
||||
}
|
||||
// Only in progress status receives any updates so the function should not return any
|
||||
// values.
|
||||
final ContentValues values =
|
||||
attachmentService.mServiceCallback.getAttachmentUpdateValues(attachment,
|
||||
EmailServiceStatus.SUCCESS, 75);
|
||||
assertTrue(values.size() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isConnected() { return true; }
|
||||
public void testServiceCallbackAttachmentErrorUpdate() {
|
||||
final AttachmentService attachmentService = new AttachmentService();
|
||||
final EmailContent.Attachment attachment = new EmailContent.Attachment();
|
||||
attachment.mSize = 1000;
|
||||
|
||||
@Override
|
||||
void cancelDownload(final DownloadRequest req) {
|
||||
mCalledCancel = true;
|
||||
}
|
||||
// Only in progress status receives any updates so the function should not return any
|
||||
// values.
|
||||
final ContentValues values =
|
||||
attachmentService.mServiceCallback.getAttachmentUpdateValues(attachment,
|
||||
EmailServiceStatus.CONNECTION_ERROR, 75);
|
||||
assertTrue(values.size() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
void processQueue() {
|
||||
mCalledProcessQueue = true;
|
||||
}
|
||||
public void testServiceCallbackAttachmentInProgressUpdate() {
|
||||
final AttachmentService attachmentService = new AttachmentService();
|
||||
final EmailContent.Attachment attachment = new EmailContent.Attachment();
|
||||
attachment.mSize = 1000;
|
||||
|
||||
// Forcing this to be false so we don't reset the alarm during testing.
|
||||
@Override
|
||||
boolean areDownloadsInProgress() { return false; }
|
||||
// Only in progress status receives any updates so this should send us some valid
|
||||
// values in return.
|
||||
final ContentValues values =
|
||||
attachmentService.mServiceCallback.getAttachmentUpdateValues(attachment,
|
||||
EmailServiceStatus.IN_PROGRESS, 75);
|
||||
|
||||
assertTrue(values.size() == 2);
|
||||
assertTrue(values.containsKey(EmailContent.AttachmentColumns.UI_STATE));
|
||||
assertTrue(values.containsKey(EmailContent.AttachmentColumns.UI_DOWNLOADED_SIZE));
|
||||
|
||||
assertTrue(values.getAsInteger(EmailContent.AttachmentColumns.UI_STATE) ==
|
||||
UIProvider.AttachmentState.DOWNLOADING);
|
||||
assertTrue(values.getAsInteger(
|
||||
EmailContent.AttachmentColumns.UI_DOWNLOADED_SIZE).intValue() == 750);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user