Simplify AttachmentDownloadService; add unit tests
* Changed our queue from a TreeMap to a TreeSet that uses an easily testable comparator * Remove the ugly bit twiddling priority computation * Test DownloadSet (the logic behind queue ordering, addition, removal, query, etc.) Change-Id: Ia8427900b8f39a243a5407349775802d0a4fad4f
This commit is contained in:
parent
567ed19f7b
commit
f19f9cf4d3
@ -22,6 +22,7 @@ import com.android.email.service.IEmailServiceCallback;
|
||||
import com.android.exchange.CalendarSyncEnabler;
|
||||
import com.android.exchange.SyncManager;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
@ -78,7 +79,7 @@ public class ExchangeUtils {
|
||||
* class. However, there are a few places we do use the service even if there's no exchange
|
||||
* accounts (e.g. setLogging), so this class is added for safety and simplicity.
|
||||
*/
|
||||
private static class NullEmailService implements IEmailService {
|
||||
public static class NullEmailService extends Service implements IEmailService {
|
||||
public static final NullEmailService INSTANCE = new NullEmailService();
|
||||
|
||||
public Bundle autoDiscover(String userName, String password) throws RemoteException {
|
||||
@ -134,5 +135,10 @@ public class ExchangeUtils {
|
||||
public IBinder asBinder() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,14 @@
|
||||
|
||||
package com.android.email.service;
|
||||
|
||||
import com.android.email.Controller;
|
||||
import com.android.email.Email;
|
||||
import com.android.email.R;
|
||||
import com.android.email.Utility;
|
||||
import com.android.email.ExchangeUtils.NullEmailService;
|
||||
import com.android.email.activity.Welcome;
|
||||
import com.android.email.provider.AttachmentProvider;
|
||||
import com.android.email.provider.EmailContent;
|
||||
import com.android.email.provider.EmailProvider;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.Attachment;
|
||||
import com.android.email.provider.EmailContent.Message;
|
||||
@ -32,14 +33,10 @@ import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.IntentFilter.MalformedMimeTypeException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.text.format.DateUtils;
|
||||
@ -47,8 +44,10 @@ import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class AttachmentDownloadService extends Service implements Runnable {
|
||||
public static final String TAG = "AttachmentService";
|
||||
@ -56,11 +55,14 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
// Our idle time, waiting for notifications; this is something of a failsafe
|
||||
private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
|
||||
|
||||
private static final long PRIORITY_NONE = -1;
|
||||
private static final int PRIORITY_NONE = -1;
|
||||
@SuppressWarnings("unused")
|
||||
private static final long PRIORITY_LOW = 0;
|
||||
private static final long PRIORITY_NORMAL = 1;
|
||||
private static final long PRIORITY_HIGH = 2;
|
||||
// Low priority will be used for opportunistic downloads
|
||||
private static final int PRIORITY_LOW = 0;
|
||||
// Normal priority is for forwarded downloads in outgoing mail
|
||||
private static final int PRIORITY_NORMAL = 1;
|
||||
// High priority is for user requests
|
||||
private static final int PRIORITY_HIGH = 2;
|
||||
|
||||
// We can try various values here; I think 2 is completely reasonable as a first pass
|
||||
private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
|
||||
@ -68,21 +70,22 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
// Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
|
||||
private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
|
||||
|
||||
private static AttachmentDownloadService sRunningService = null;
|
||||
/*package*/ static AttachmentDownloadService sRunningService = null;
|
||||
|
||||
private AttachmentReceiver mAttachmentReceiver;
|
||||
private Context mContext;
|
||||
private final AttachmentMap mAttachmentMap = new AttachmentMap();
|
||||
/*package*/ Context mContext;
|
||||
/*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator());
|
||||
private final HashMap<Long, Class<? extends Service>> mAccountServiceMap =
|
||||
new HashMap<Long, Class<? extends Service>>();
|
||||
private final ServiceCallback mServiceCallback = new ServiceCallback();
|
||||
private final Object mLock = new Object();
|
||||
private volatile boolean mStop = false;
|
||||
|
||||
private static class DownloadRequest {
|
||||
long attachmentId;
|
||||
long messageId = -1;
|
||||
long accountId;
|
||||
public static class DownloadRequest {
|
||||
final int priority;
|
||||
final long time;
|
||||
final long attachmentId;
|
||||
final long messageId;
|
||||
final long accountId;
|
||||
boolean inProgress = false;
|
||||
|
||||
private DownloadRequest(Context context, Attachment attachment) {
|
||||
@ -91,41 +94,70 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
if (msg != null) {
|
||||
accountId = msg.mAccountKey;
|
||||
messageId = msg.mId;
|
||||
} else {
|
||||
accountId = messageId = -1;
|
||||
}
|
||||
priority = getPriority(attachment);
|
||||
time = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)attachmentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two download requests are equals if their attachment id's are equals
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (!(object instanceof DownloadRequest)) return false;
|
||||
DownloadRequest req = (DownloadRequest)object;
|
||||
return req.attachmentId == attachmentId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The AttachmentMap is a TreeMap sorted by "priority key", which is determined first by the
|
||||
* priority class (e.g. low, high, etc.) and the time of the request. Higher priority requests
|
||||
* Comparator class for the download set; we first compare by priority. Requests with equal
|
||||
* priority are compared by the time the request was created (older requests come first)
|
||||
*/
|
||||
/*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> {
|
||||
@Override
|
||||
public int compare(DownloadRequest req1, DownloadRequest req2) {
|
||||
int res;
|
||||
if (req1.priority != req2.priority) {
|
||||
res = (req1.priority < req2.priority) ? -1 : 1;
|
||||
} else {
|
||||
if (req1.time == req2.time) {
|
||||
res = 0;
|
||||
} else {
|
||||
res = (req1.time > req2.time) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
//Log.d(TAG, "Compare " + req1.attachmentId + " to " + req2.attachmentId + " = " + res);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the
|
||||
* time of the request. Higher priority requests
|
||||
* are always processed first; among equals, the oldest request is processed first. The
|
||||
* priority key represents this ordering. Note: All methods that change the attachment map are
|
||||
* synchronized on the map itself
|
||||
*/
|
||||
private class AttachmentMap extends TreeMap<Long, DownloadRequest> {
|
||||
/*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final HashMap<Long, DownloadRequest> mDownloadsInProgress =
|
||||
new HashMap<Long, DownloadRequest>();
|
||||
/*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) {
|
||||
super(comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Maps attachment id to DownloadRequest
|
||||
*/
|
||||
private long getPriorityKey(Attachment att) {
|
||||
long priorityClass = PRIORITY_NONE;
|
||||
int flags = att.mFlags;
|
||||
if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
||||
priorityClass = PRIORITY_NORMAL;
|
||||
} else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
|
||||
priorityClass = PRIORITY_HIGH;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
return (priorityKey(priorityClass, System.currentTimeMillis()));
|
||||
}
|
||||
/*package*/ final HashMap<Long, DownloadRequest> mDownloadsInProgress =
|
||||
new HashMap<Long, DownloadRequest>();
|
||||
|
||||
/**
|
||||
* onChange is called by the AttachmentReceiver upon receipt of a valid notification from
|
||||
@ -134,91 +166,72 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
* existence of an attachment before acting on it.
|
||||
*/
|
||||
public synchronized void onChange(Attachment att) {
|
||||
long attKey = findPriorityKey(att.mId);
|
||||
long priorityKey = getPriorityKey(att);
|
||||
if (priorityKey == 0) {
|
||||
DownloadRequest req = findDownloadRequest(att.mId);
|
||||
long priority = getPriority(att);
|
||||
if (priority == PRIORITY_NONE) {
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== Attachment changed: " + att.mId);
|
||||
}
|
||||
// In this case, there is no download priority for this attachment
|
||||
if (attKey != 0) {
|
||||
// If it exists in the map, remove it and try to stop any download in progress
|
||||
if (req != null) {
|
||||
// If it exists in the map, remove it
|
||||
// NOTE: We don't yet support deleting downloads in progress
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
|
||||
}
|
||||
remove(attKey, true);
|
||||
remove(req);
|
||||
}
|
||||
} else {
|
||||
// Ignore changes that occur during download
|
||||
if (mDownloadsInProgress.containsKey(att.mId)) return;
|
||||
// Remove from the map, but don't stop a download in progress
|
||||
DownloadRequest req = remove(attKey, false);
|
||||
// If this is new, add the request to the queue
|
||||
if (req == null) {
|
||||
req = new DownloadRequest(mContext, att);
|
||||
add(req);
|
||||
}
|
||||
// If the request already existed, we'll update the priority (so that the time is
|
||||
// up-to-date); otherwise, we create a new request
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
|
||||
priorityClass(priorityKey) + ", priority time " +
|
||||
priorityTime(priorityKey));
|
||||
req.priority + ", priority time " + req.time);
|
||||
}
|
||||
// Store the request away
|
||||
put(priorityKey, req);
|
||||
}
|
||||
// Process the queue if we're in a wait
|
||||
kick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a DownloadRequest from the queue, given its priority key
|
||||
* @param priorityKey the priority key to remove (a Long)
|
||||
* @param stopDownload whether we should try to stop an in-progress download of the
|
||||
* attachment with this priority key (not implemented)
|
||||
* @return the DownloadRequest for this priority key (or null, if none)
|
||||
*/
|
||||
public synchronized DownloadRequest remove(Object priorityKey, boolean stopDownload) {
|
||||
DownloadRequest req = remove(priorityKey);
|
||||
if ((req != null) && req.inProgress && stopDownload) {
|
||||
// TODO: Stop download
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== Download of " + req.attachmentId + " stopped; NOT implemented");
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the priority key of a queued DownloadRequest, given the attachment's id
|
||||
* Find a queued DownloadRequest, given the attachment's id
|
||||
* @param id the id of the attachment
|
||||
* @return the priority key for that attachment (or zero, if none)
|
||||
* @return the DownloadRequest for that attachment (or null, if none)
|
||||
*/
|
||||
private synchronized long findPriorityKey(long id) {
|
||||
for (Entry<Long, DownloadRequest> entry: entrySet()) {
|
||||
if (entry.getValue().attachmentId == id) {
|
||||
return entry.getKey();
|
||||
/*package*/ synchronized DownloadRequest findDownloadRequest(long id) {
|
||||
Iterator<DownloadRequest> iterator = iterator();
|
||||
while(iterator.hasNext()) {
|
||||
DownloadRequest req = iterator.next();
|
||||
if (req.attachmentId == id) {
|
||||
return req;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
|
||||
* the limit on maximum downloads
|
||||
*/
|
||||
private synchronized void processQueue() {
|
||||
/*package*/ synchronized void processQueue() {
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== Checking attachment queue, " + mAttachmentMap.size() + " entries");
|
||||
Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries");
|
||||
}
|
||||
Entry<Long, DownloadRequest> entry = mAttachmentMap.lastEntry();
|
||||
Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
|
||||
// First, start up any required downloads, in priority order
|
||||
while ((entry != null) && (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
|
||||
DownloadRequest req = entry.getValue();
|
||||
while (iterator.hasNext() &&
|
||||
(mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
|
||||
DownloadRequest req = iterator.next();
|
||||
if (!req.inProgress) {
|
||||
mAttachmentMap.tryStartDownload(req);
|
||||
mDownloadSet.tryStartDownload(req);
|
||||
}
|
||||
entry = mAttachmentMap.lowerEntry(entry.getKey());
|
||||
}
|
||||
// Then, try opportunistic download of appropriate attachments
|
||||
int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
|
||||
@ -236,7 +249,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
* @param accountId the id of the account
|
||||
* @return the count of running downloads
|
||||
*/
|
||||
private int downloadsForAccount(long accountId) {
|
||||
/*package*/ synchronized int downloadsForAccount(long accountId) {
|
||||
int count = 0;
|
||||
for (DownloadRequest req: mDownloadsInProgress.values()) {
|
||||
if (req.accountId == accountId) {
|
||||
@ -252,7 +265,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
* @param req the DownloadRequest
|
||||
* @return whether or not the download was started
|
||||
*/
|
||||
private synchronized boolean tryStartDownload(DownloadRequest req) {
|
||||
/*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
|
||||
// Enforce per-account limit
|
||||
if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
|
||||
if (Email.DEBUG) {
|
||||
@ -271,9 +284,12 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
|
||||
}
|
||||
proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(),
|
||||
AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId)
|
||||
// Don't actually run the load if this is the NullEmailService (used in unit tests)
|
||||
if (!serviceClass.equals(NullEmailService.class)) {
|
||||
proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(),
|
||||
AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId)
|
||||
.toString());
|
||||
}
|
||||
mDownloadsInProgress.put(req.attachmentId, req);
|
||||
req.inProgress = true;
|
||||
} catch (RemoteException e) {
|
||||
@ -290,27 +306,26 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
* @param attachmentId the id of the attachment whose download is finished
|
||||
* @param statusCode the EmailServiceStatus code returned by the Service
|
||||
*/
|
||||
private synchronized void endDownload(long attachmentId, int statusCode) {
|
||||
/*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
|
||||
// Say we're no longer downloading this
|
||||
mDownloadsInProgress.remove(attachmentId);
|
||||
long priorityKey = mAttachmentMap.findPriorityKey(attachmentId);
|
||||
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
|
||||
if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
|
||||
// If this needs to be retried, just process the queue again
|
||||
if (Email.DEBUG) {
|
||||
Log.d(TAG, "== The download for attachment #" + attachmentId +
|
||||
" will be retried");
|
||||
}
|
||||
DownloadRequest req = mAttachmentMap.get(priorityKey);
|
||||
if (req != null) {
|
||||
req.inProgress = false;
|
||||
}
|
||||
kick();
|
||||
return;
|
||||
}
|
||||
DownloadRequest req = mAttachmentMap.remove(priorityKey);
|
||||
// Remove the request from the queue
|
||||
remove(req);
|
||||
if (Email.DEBUG) {
|
||||
long secs = (priorityTime(System.currentTimeMillis()) -
|
||||
priorityKeyToSystemTime(priorityKey)) / 1000;
|
||||
long secs = (System.currentTimeMillis() - req.time) / 1000;
|
||||
String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
|
||||
"Error " + statusCode;
|
||||
Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs +
|
||||
@ -321,7 +336,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
boolean deleted = false;
|
||||
if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
||||
if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
|
||||
// If this is a forwarding download, and the attachment doesn't exist (or
|
||||
// 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);
|
||||
@ -356,26 +371,21 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
private static int systemTimeToPriorityTime(long systemTime) {
|
||||
return Integer.MAX_VALUE - priorityTime(systemTime);
|
||||
}
|
||||
|
||||
private static long priorityKeyToSystemTime(long priorityKey) {
|
||||
return Integer.MAX_VALUE - priorityTime(priorityKey);
|
||||
}
|
||||
|
||||
private static int priorityTime(long priorityKeyOrSystemTime) {
|
||||
return (int)(priorityKeyOrSystemTime & 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
private static long priorityClass(long priorityKey) {
|
||||
return priorityKey >> 32;
|
||||
}
|
||||
|
||||
private static long priorityKey(long priorityClass, long timeInMillis) {
|
||||
// High 32 bits = priority class
|
||||
// Low 32 bits = priority time
|
||||
return (priorityClass<<32) | systemTimeToPriorityTime(timeInMillis);
|
||||
/**
|
||||
* 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(Attachment att) {
|
||||
int priorityClass = PRIORITY_NONE;
|
||||
int flags = att.mFlags;
|
||||
if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
|
||||
priorityClass = PRIORITY_NORMAL;
|
||||
} else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
|
||||
priorityClass = PRIORITY_HIGH;
|
||||
}
|
||||
return priorityClass;
|
||||
}
|
||||
|
||||
private void kick() {
|
||||
@ -384,39 +394,6 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The AttachmentReceiver handles broadcasts from EmailProvider when an attachment is inserted
|
||||
* or updated. Assuming that the attachment still exists, we just call the AttachmentMap's
|
||||
* onChange() method
|
||||
*/
|
||||
public class AttachmentReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri attachmentUri = intent.getData();
|
||||
String action = intent.getAction();
|
||||
if (action.equals(EmailProvider.ACTION_ATTACHMENT_UPDATED)) {
|
||||
// We need to look at the flags and see if loading is appropriate
|
||||
final int flags =
|
||||
intent.getIntExtra(EmailProvider.ATTACHMENT_UPDATED_EXTRA_FLAGS, -1);
|
||||
// If the flags didn't change, we're done
|
||||
if (flags == -1) return;
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
long id = Long.parseLong(attachmentUri.getLastPathSegment());
|
||||
Attachment attachment =
|
||||
Attachment.restoreAttachmentWithId(AttachmentDownloadService.this, id);
|
||||
if (attachment != null) {
|
||||
// Store the flags we got from EmailProvider; given that all of this
|
||||
// activity is asynchronous, we need to use the newest data from
|
||||
// EmailProvider
|
||||
attachment.mFlags = flags;
|
||||
mAttachmentMap.onChange(attachment);
|
||||
}
|
||||
}}).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks
|
||||
* come from either Controller (IMAP) or SyncManager (EAS). Note that we only implement the
|
||||
@ -446,7 +423,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
case EmailServiceStatus.IN_PROGRESS:
|
||||
break;
|
||||
default:
|
||||
mAttachmentMap.endDownload(attachmentId, statusCode);
|
||||
mDownloadSet.endDownload(attachmentId, statusCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -507,26 +484,32 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
if (protocol.equals("eas")) {
|
||||
serviceClass = SyncManager.class;
|
||||
} else {
|
||||
serviceClass = com.android.email.Controller.class;
|
||||
serviceClass = Controller.class;
|
||||
}
|
||||
mAccountServiceMap.put(accountId, serviceClass);
|
||||
}
|
||||
return serviceClass;
|
||||
}
|
||||
|
||||
private void onChange(Attachment att) {
|
||||
mAttachmentMap.onChange(att);
|
||||
/*protected*/ void addServiceClass(long accountId, Class<? extends Service> serviceClass) {
|
||||
mAccountServiceMap.put(accountId, serviceClass);
|
||||
}
|
||||
|
||||
private boolean isQueued(long attachmentId) {
|
||||
return mAttachmentMap.findPriorityKey(attachmentId) != 0;
|
||||
/*package*/ void onChange(Attachment att) {
|
||||
mDownloadSet.onChange(att);
|
||||
}
|
||||
|
||||
private boolean dequeue(long attachmentId) {
|
||||
long priority = mAttachmentMap.findPriorityKey(attachmentId);
|
||||
if (priority == 0) return false;
|
||||
DownloadRequest req = mAttachmentMap.remove(priority);
|
||||
return req != null;
|
||||
/*package*/ boolean isQueued(long attachmentId) {
|
||||
return mDownloadSet.findDownloadRequest(attachmentId) != null;
|
||||
}
|
||||
|
||||
/*package*/ boolean dequeue(long attachmentId) {
|
||||
DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
|
||||
if (req != null) {
|
||||
mDownloadSet.remove(req);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -559,6 +542,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
* @param flags the new flags for the attachment
|
||||
*/
|
||||
public static void attachmentChanged(final long id, final int flags) {
|
||||
if (sRunningService == null) return;
|
||||
Utility.runAsync(new Runnable() {
|
||||
public void run() {
|
||||
final Attachment attachment =
|
||||
@ -575,16 +559,6 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
|
||||
public void run() {
|
||||
mContext = this;
|
||||
// Set up our receiver for EmailProvider attachment updates
|
||||
mAttachmentReceiver = new AttachmentReceiver();
|
||||
try {
|
||||
getApplicationContext().registerReceiver(mAttachmentReceiver,
|
||||
new IntentFilter(EmailProvider.ACTION_ATTACHMENT_UPDATED,
|
||||
EmailProvider.EMAIL_ATTACHMENT_MIME_TYPE));
|
||||
} catch (MalformedMimeTypeException e1) {
|
||||
// Since we're passing in a constant mime type, this can't happen
|
||||
}
|
||||
|
||||
// Run through all attachments in the database that require download and add them to
|
||||
// the queue
|
||||
int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
|
||||
@ -597,7 +571,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
Attachment attachment = Attachment.restoreAttachmentWithId(
|
||||
this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
|
||||
if (attachment != null) {
|
||||
mAttachmentMap.onChange(attachment);
|
||||
mDownloadSet.onChange(attachment);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -610,7 +584,7 @@ public class AttachmentDownloadService extends Service implements Runnable {
|
||||
// Loop until stopped, with a 30 minute wait loop
|
||||
while (!mStop) {
|
||||
// Here's where we run our attachment loading logic...
|
||||
mAttachmentMap.processQueue();
|
||||
mDownloadSet.processQueue();
|
||||
synchronized(mLock) {
|
||||
try {
|
||||
mLock.wait(PROCESS_QUEUE_WAIT_TIME);
|
||||
|
@ -30,7 +30,6 @@ import android.test.MoreAsserts;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
@ -198,11 +197,12 @@ public class ProviderTestUtils extends Assert {
|
||||
* @param messageId the message to attach to
|
||||
* @param fileName the "file" to indicate in the attachment
|
||||
* @param length the "length" of the attachment
|
||||
* @param flags the flags to set in the attachment
|
||||
* @param saveIt if true, write the new attachment directly to the DB
|
||||
* @param context use this context
|
||||
*/
|
||||
public static Attachment setupAttachment(long messageId, String fileName, long length,
|
||||
boolean saveIt, Context context) {
|
||||
int flags, boolean saveIt, Context context) {
|
||||
Attachment att = new Attachment();
|
||||
att.mSize = length;
|
||||
att.mFileName = fileName;
|
||||
@ -213,7 +213,7 @@ public class ProviderTestUtils extends Assert {
|
||||
att.mLocation = "location " + fileName;
|
||||
att.mEncoding = "encoding " + fileName;
|
||||
att.mContent = "content " + fileName;
|
||||
att.mFlags = 0;
|
||||
att.mFlags = flags;
|
||||
att.mContentBytes = Utility.toUtf8("content " + fileName);
|
||||
if (saveIt) {
|
||||
att.save(context);
|
||||
@ -221,6 +221,20 @@ public class ProviderTestUtils extends Assert {
|
||||
return att;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test attachment with flags = 0 (see above)
|
||||
*
|
||||
* @param messageId the message to attach to
|
||||
* @param fileName the "file" to indicate in the attachment
|
||||
* @param length the "length" of the attachment
|
||||
* @param saveIt if true, write the new attachment directly to the DB
|
||||
* @param context use this context
|
||||
*/
|
||||
public static Attachment setupAttachment(long messageId, String fileName, long length,
|
||||
boolean saveIt, Context context) {
|
||||
return setupAttachment(messageId, fileName, length, 0, saveIt, context);
|
||||
}
|
||||
|
||||
private static void assertEmailContentEqual(String caller, EmailContent expect,
|
||||
EmailContent actual) {
|
||||
if (expect == actual) {
|
||||
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.email.service;
|
||||
|
||||
import com.android.email.AccountTestCase;
|
||||
import com.android.email.ExchangeUtils.NullEmailService;
|
||||
import com.android.email.provider.ProviderTestUtils;
|
||||
import com.android.email.provider.EmailContent.Account;
|
||||
import com.android.email.provider.EmailContent.Attachment;
|
||||
import com.android.email.provider.EmailContent.Mailbox;
|
||||
import com.android.email.provider.EmailContent.Message;
|
||||
import com.android.email.service.AttachmentDownloadService.DownloadRequest;
|
||||
import com.android.email.service.AttachmentDownloadService.DownloadSet;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Tests of the AttachmentDownloadService
|
||||
*
|
||||
* You can run this entire test case with:
|
||||
* runtest -c com.android.email.service.AttachmentDownloadServiceTests email
|
||||
*/
|
||||
public class AttachmentDownloadServiceTests extends AccountTestCase {
|
||||
private AttachmentDownloadService mService;
|
||||
private Context mMockContext;
|
||||
private Account mAccount;
|
||||
private Mailbox mMailbox;
|
||||
private long mAccountId;
|
||||
private long mMailboxId;
|
||||
private DownloadSet mDownloadSet;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
|
||||
// Set up an account and mailbox
|
||||
mAccount = ProviderTestUtils.setupAccount("account", false, mMockContext);
|
||||
mAccount.save(mMockContext);
|
||||
mAccountId = mAccount.mId;
|
||||
mMailbox = ProviderTestUtils.setupMailbox("mailbox", mAccountId, true, mMockContext);
|
||||
mMailboxId = mMailbox.mId;
|
||||
|
||||
// Set up our download service to simulate a running environment
|
||||
// Use the NullEmailService so that the loadAttachment calls become no-ops
|
||||
mService = new AttachmentDownloadService();
|
||||
mService.mContext = mMockContext;
|
||||
mService.addServiceClass(mAccountId, NullEmailService.class);
|
||||
mDownloadSet = mService.mDownloadSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* This test creates attachments and places them in the DownloadSet; we then do various checks
|
||||
* that exercise its functionality.
|
||||
*/
|
||||
public void testDownloadSet() {
|
||||
// TODO: Make sure that this doesn't interfere with the "real" ADS that might be running
|
||||
// on device
|
||||
Message message = ProviderTestUtils.setupMessage("message", mAccountId, mMailboxId, false,
|
||||
true, mMockContext);
|
||||
Attachment att1 = ProviderTestUtils.setupAttachment(message.mId, "filename1", 1000,
|
||||
Attachment.FLAG_DOWNLOAD_USER_REQUEST, true, mMockContext);
|
||||
Attachment att2 = ProviderTestUtils.setupAttachment(message.mId, "filename2", 1000,
|
||||
Attachment.FLAG_DOWNLOAD_FORWARD, true, mMockContext);
|
||||
Attachment att3 = ProviderTestUtils.setupAttachment(message.mId, "filename3", 1000,
|
||||
Attachment.FLAG_DOWNLOAD_FORWARD, true, mMockContext);
|
||||
Attachment att4 = ProviderTestUtils.setupAttachment(message.mId, "filename4", 1000,
|
||||
Attachment.FLAG_DOWNLOAD_USER_REQUEST, true, mMockContext);
|
||||
// Indicate that these attachments have changed; they will be added to the queue
|
||||
mDownloadSet.onChange(att1);
|
||||
mDownloadSet.onChange(att2);
|
||||
mDownloadSet.onChange(att3);
|
||||
mDownloadSet.onChange(att4);
|
||||
Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
|
||||
// Check the expected ordering; 1 & 4 are higher priority than 2 & 3
|
||||
// 1 and 3 were created earlier than their priority equals
|
||||
long[] expectedAttachmentIds = new long[] {att1.mId, att4.mId, att2.mId, att3.mId};
|
||||
for (int i = 0; i < expectedAttachmentIds.length; i++) {
|
||||
assertTrue(iterator.hasNext());
|
||||
DownloadRequest req = iterator.next();
|
||||
assertEquals(expectedAttachmentIds[i], req.attachmentId);
|
||||
}
|
||||
|
||||
// Process the queue; attachment 1 should be marked "in progress", and should be in
|
||||
// the in-progress map
|
||||
mDownloadSet.processQueue();
|
||||
DownloadRequest req = mDownloadSet.findDownloadRequest(att1.mId);
|
||||
assertNotNull(req);
|
||||
assertTrue(req.inProgress);
|
||||
assertTrue(mDownloadSet.mDownloadsInProgress.containsKey(att1.mId));
|
||||
// There should also be only one download in progress (testing the per-account limitation)
|
||||
assertEquals(1, mDownloadSet.mDownloadsInProgress.size());
|
||||
// End the "download" with a connection error; we should still have this in the queue,
|
||||
// but it should no longer be in-progress
|
||||
mDownloadSet.endDownload(att1.mId, EmailServiceStatus.CONNECTION_ERROR);
|
||||
assertFalse(req.inProgress);
|
||||
assertEquals(0, mDownloadSet.mDownloadsInProgress.size());
|
||||
|
||||
mDownloadSet.processQueue();
|
||||
// Things should be as they were earlier; att1 should be an in-progress download
|
||||
req = mDownloadSet.findDownloadRequest(att1.mId);
|
||||
assertNotNull(req);
|
||||
assertTrue(req.inProgress);
|
||||
assertTrue(mDownloadSet.mDownloadsInProgress.containsKey(att1.mId));
|
||||
// Successfully download the attachment; there should be no downloads in progress, and
|
||||
// att1 should no longer be in the queue
|
||||
mDownloadSet.endDownload(att1.mId, EmailServiceStatus.SUCCESS);
|
||||
assertEquals(0, mDownloadSet.mDownloadsInProgress.size());
|
||||
assertNull(mDownloadSet.findDownloadRequest(att1.mId));
|
||||
|
||||
// Test dequeue and isQueued
|
||||
assertEquals(3, mDownloadSet.size());
|
||||
mService.dequeue(att2.mId);
|
||||
assertEquals(2, mDownloadSet.size());
|
||||
assertTrue(mService.isQueued(att4.mId));
|
||||
assertTrue(mService.isQueued(att3.mId));
|
||||
|
||||
mDownloadSet.processQueue();
|
||||
// att4 should be the download in progress
|
||||
req = mDownloadSet.findDownloadRequest(att4.mId);
|
||||
assertNotNull(req);
|
||||
assertTrue(req.inProgress);
|
||||
assertTrue(mDownloadSet.mDownloadsInProgress.containsKey(att4.mId));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user