summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnthony Lee <anthonylee@google.com>2014-06-25 09:56:29 -0700
committerAnthony Lee <anthonylee@google.com>2014-06-25 09:56:29 -0700
commit3d16e5d4b994d92db51962c8c461c53bee04309f (patch)
tree881eb72bb785e8bb8959720d24dcff068ca81d34
parentff52eef8e691645e657f02a21c9d51968c2bdcfd (diff)
downloadandroid_packages_apps_Email-3d16e5d4b994d92db51962c8c461c53bee04309f.tar.gz
android_packages_apps_Email-3d16e5d4b994d92db51962c8c461c53bee04309f.tar.bz2
android_packages_apps_Email-3d16e5d4b994d92db51962c8c461c53bee04309f.zip
Move over to the new AttachmentService.
Change-Id: I4a687b003884ea6a92a755fcbf394001bfc96a38
-rw-r--r--AndroidManifest.xml4
-rwxr-xr-xemailcommon/src/com/android/emailcommon/provider/EmailContent.java2
-rw-r--r--proguard.flags23
-rw-r--r--src/com/android/email/provider/EmailProvider.java16
-rw-r--r--src/com/android/email/service/AttachmentDownloadService.java1143
-rw-r--r--src/com/android/email/service/EmailServiceStub.java2
-rw-r--r--src/com/android/email2/ui/MailActivityEmail.java8
-rw-r--r--tests/src/com/android/email/provider/ProviderTests.java5
-rw-r--r--tests/src/com/android/email/service/AttachmentDownloadServiceTests.java283
9 files changed, 20 insertions, 1466 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index caac4d492..4ddf48243 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -424,7 +424,7 @@
<!-- From Email application -->
<receiver
- android:name=".service.AttachmentDownloadService$Watchdog"
+ android:name=".service.AttachmentService$AttachmentWatchdog"
android:enabled="true"/>
<!-- Handles app upgrade. This disables itself after running once. -->
@@ -482,7 +482,7 @@
</receiver>
<service
- android:name=".service.AttachmentDownloadService"
+ android:name=".service.AttachmentService"
android:enabled="false"
>
</service>
diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
index 0ae908d7e..4d3fffcdf 100755
--- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
+++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java
@@ -1407,7 +1407,7 @@ public abstract class EmailContent {
+ ")";
// Bits used in mFlags
- // WARNING: AttachmentDownloadService relies on the fact that ALL of the flags below
+ // WARNING: AttachmentService relies on the fact that ALL of the flags below
// disqualify attachments for precaching. If you add a flag that does NOT disqualify an
// attachment for precaching, you MUST change the PRECACHE_SELECTION definition above
diff --git a/proguard.flags b/proguard.flags
index 2c3062403..6b0a84488 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -60,14 +60,6 @@
*** createUniqueFile(java.lang.String);
}
--keepclasseswithmembers class com.android.email.service.AttachmentDownloadService {
- *** addServiceClass(long, java.lang.Class);
-}
-
--keepclasseswithmembers class com.android.email.service.AttachmentDownloadService$AccountManagerStub {
- *** setNumberOfAccounts(int);
-}
-
-keepclasseswithmembers class com.android.email.Preferences {
*** getAccountByContentUri(android.net.Uri);
}
@@ -130,21 +122,6 @@
-keep class com.android.emailcommon.mail.Flag
--keepclasseswithmembers class com.android.email.service.AttachmentService$DownloadQueue {
- *** addRequest(com.android.email.service.AttachmentService$DownloadRequest);
- *** removeRequest(com.android.email.service.AttachmentService$DownloadRequest);
- *** getNextRequest();
- *** findRequestById(long);
- *** getSize();
- *** isEmpty();
-}
-
--keepclasseswithmembers class com.android.email.service.AttachmentService$DownloadRequest {
- <init>(int, long);
- *** hashCode();
- *** equals(java.lang.Object);
-}
-
-keepclasseswithmembers class com.android.emailcommon.mail.Folder {
*** getUnreadMessageCount();
*** delete(boolean);
diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java
index e89dff37c..db735c881 100644
--- a/src/com/android/email/provider/EmailProvider.java
+++ b/src/com/android/email/provider/EmailProvider.java
@@ -68,7 +68,7 @@ import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.email.activity.setup.AccountSettingsFragment;
import com.android.email.activity.setup.AccountSettingsUtils;
-import com.android.email.service.AttachmentDownloadService;
+import com.android.email.service.AttachmentService;
import com.android.email.service.EmailServiceUtils;
import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
import com.android.email2.ui.MailActivityEmail;
@@ -2439,24 +2439,24 @@ public class EmailProvider extends ContentProvider
}
}
- public static interface AttachmentService {
+ public static interface EmailAttachmentService {
/**
* Notify the service that an attachment has changed.
*/
- void attachmentChanged(Context context, long id, int flags);
+ void attachmentChanged(final Context context, final long id, final int flags);
}
- private final AttachmentService DEFAULT_ATTACHMENT_SERVICE = new AttachmentService() {
+ private final EmailAttachmentService DEFAULT_ATTACHMENT_SERVICE = new EmailAttachmentService() {
@Override
- public void attachmentChanged(Context context, long id, int flags) {
+ public void attachmentChanged(final Context context, final long id, final int flags) {
// The default implementation delegates to the real service.
- AttachmentDownloadService.attachmentChanged(context, id, flags);
+ AttachmentService.attachmentChanged(context, id, flags);
}
};
- private AttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
+ private EmailAttachmentService mAttachmentService = DEFAULT_ATTACHMENT_SERVICE;
// exposed for testing
- public void injectAttachmentService(AttachmentService attachmentService) {
+ public void injectAttachmentService(final EmailAttachmentService attachmentService) {
mAttachmentService =
attachmentService == null ? DEFAULT_ATTACHMENT_SERVICE : attachmentService;
}
diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java
deleted file mode 100644
index 5a7c4ac20..000000000
--- a/src/com/android/email/service/AttachmentDownloadService.java
+++ /dev/null
@@ -1,1143 +0,0 @@
-/*
- * 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 android.accounts.AccountManager;
-import android.app.AlarmManager;
-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.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.text.format.DateUtils;
-
-import com.android.email.AttachmentInfo;
-import com.android.email.EmailConnectivityManager;
-import com.android.email.NotificationController;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.service.IEmailServiceCallback;
-import com.android.emailcommon.utility.AttachmentUtilities;
-import com.android.emailcommon.utility.Utility;
-import com.android.mail.providers.UIProvider.AttachmentState;
-import com.android.mail.utils.LogUtils;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Queue;
-import java.util.TreeSet;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-public class AttachmentDownloadService extends Service implements Runnable {
- public static final String TAG = LogUtils.TAG;
-
- // Minimum wait time before retrying a download that failed due to connection error
- private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS;
- // Number of retries before we start delaying between
- private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5;
- // Maximum time to retry for connection errors.
- private static final long CONNECTION_ERROR_MAX_RETRIES = 10;
-
- // 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);
- // How often our watchdog checks for callback timeouts
- private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
- // How long we'll wait for a callback before canceling a download and retrying
- private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
- // Try to download an attachment in the background this many times before giving up
- private static final int MAX_DOWNLOAD_RETRIES = 5;
- private static final int PRIORITY_NONE = -1;
- @SuppressWarnings("unused")
- // Low priority will be used for opportunistic downloads
- private static final int PRIORITY_BACKGROUND = 0;
- // Normal priority is for forwarded downloads in outgoing mail
- private static final int PRIORITY_SEND_MAIL = 1;
- // High priority is for user requests
- private static final int PRIORITY_FOREGROUND = 2;
-
- // Minimum free storage in order to perform prefetch (25% of total memory)
- private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F;
- // Maximum prefetch storage (also 25% of total memory)
- private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F;
-
- // We can try various values here; I think 2 is completely reasonable as a first pass
- private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
- // Limit on the number of simultaneous downloads per account
- // 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;
- // Limit on the number of attachments we'll check for background download
- private static final int MAX_ATTACHMENTS_TO_CHECK = 25;
-
- private static final String EXTRA_ATTACHMENT =
- "com.android.email.AttachmentDownloadService.attachment";
-
- // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
- // by the use of "volatile"
- /*package*/ static volatile AttachmentDownloadService sRunningService = null;
-
- /*package*/ Context mContext;
- /*package*/ EmailConnectivityManager mConnectivityManager;
-
- /*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator());
-
- private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
- // A map of attachment storage used per account
- // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated
- // amount plus the size of any new attachments laoded). If and when we reach the per-account
- // limit, we recalculate the actual usage
- /*package*/ final HashMap<Long, Long> mAttachmentStorageMap = new HashMap<Long, Long>();
- // A map of attachment ids to the number of failed attempts to download the attachment
- // NOTE: We do not want to persist this. This allows us to retry background downloading
- // if any transient network errors are fixed & and the app is restarted
- /* package */ final HashMap<Long, Integer> mAttachmentFailureMap = new HashMap<Long, Integer>();
- private final ServiceCallback mServiceCallback = new ServiceCallback();
-
- private final Object mLock = new Object();
- private volatile boolean mStop = false;
-
- /*package*/ AccountManagerStub mAccountManagerStub;
-
- /**
- * We only use the getAccounts() call from AccountManager, so this class wraps that call and
- * allows us to build a mock account manager stub in the unit tests
- */
- /*package*/ static class AccountManagerStub {
- private int mNumberOfAccounts;
- private final AccountManager mAccountManager;
-
- AccountManagerStub(Context context) {
- if (context != null) {
- mAccountManager = AccountManager.get(context);
- } else {
- mAccountManager = null;
- }
- }
-
- /*package*/ int getNumberOfAccounts() {
- if (mAccountManager != null) {
- return mAccountManager.getAccounts().length;
- } else {
- return mNumberOfAccounts;
- }
- }
-
- /*package*/ void setNumberOfAccounts(int numberOfAccounts) {
- mNumberOfAccounts = numberOfAccounts;
- }
- }
-
- /**
- * Watchdog alarm receiver; responsible for making sure that downloads in progress are not
- * stalled, as determined by the timing of the most recent service callback
- */
- public static class Watchdog extends BroadcastReceiver {
- @Override
- public void onReceive(final Context context, Intent intent) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- watchdogAlarm();
- }
- }, "AttachmentDownloadService Watchdog").start();
- }
- }
-
- public static class DownloadRequest {
- final int priority;
- final long time;
- final long attachmentId;
- final long messageId;
- final long accountId;
- boolean inProgress = false;
- int lastStatusCode;
- int lastProgress;
- long lastCallbackTime;
- long startTime;
- long retryCount;
- long retryStartTime;
-
- private DownloadRequest(Context context, Attachment attachment) {
- attachmentId = attachment.mId;
- Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
- if (msg != null) {
- accountId = msg.mAccountKey;
- messageId = msg.mId;
- } else {
- accountId = messageId = -1;
- }
- priority = getPriority(attachment);
- time = SystemClock.elapsedRealtime();
- }
-
- private DownloadRequest(DownloadRequest orig, long newTime) {
- priority = orig.priority;
- attachmentId = orig.attachmentId;
- messageId = orig.messageId;
- accountId = orig.accountId;
- time = newTime;
- inProgress = orig.inProgress;
- lastStatusCode = orig.lastStatusCode;
- lastProgress = orig.lastProgress;
- lastCallbackTime = orig.lastCallbackTime;
- startTime = orig.startTime;
- retryCount = orig.retryCount;
- retryStartTime = orig.retryStartTime;
- }
-
-
- @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;
- }
- }
-
- /**
- * 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;
- }
- }
- 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
- */
- /*package*/ class DownloadSet extends TreeSet<DownloadRequest> {
- private static final long serialVersionUID = 1L;
- private PendingIntent mWatchdogPendingIntent;
-
- /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) {
- super(comparator);
- }
-
- /**
- * Maps attachment id to DownloadRequest
- */
- /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
- new ConcurrentHashMap<Long, DownloadRequest>();
-
- private 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);
- }
-
- /**
- * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
- * EmailProvider that an attachment has been inserted or modified. It's not strictly
- * necessary that we detect a deleted attachment, as the code always checks for the
- * existence of an attachment before acting on it.
- */
- public synchronized void onChange(Context context, Attachment att) {
- DownloadRequest req = findDownloadRequest(att.mId);
- long priority = getPriority(att);
- if (priority == PRIORITY_NONE) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Attachment changed: " + att.mId);
- }
- // In this case, there is no download priority for this attachment
- if (req != null) {
- // If it exists in the map, remove it
- // NOTE: We don't yet support deleting downloads in progress
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Attachment " + att.mId + " was in queue, removing");
- }
- remove(req);
- }
- } else {
- // Ignore changes that occur during download
- if (mDownloadsInProgress.containsKey(att.mId)) return;
- // If this is new, add the request to the queue
- if (req == null) {
- req = new DownloadRequest(context, att);
- final AttachmentInfo attachInfo = new AttachmentInfo(context, att);
- if (!attachInfo.isEligibleForDownload()) {
- // We can't download this file due to policy, depending on what type
- // of request we received, we handle the response differently.
- if (((att.mFlags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) ||
- ((att.mFlags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0)) {
- // There are a couple of situations where we will not even allow this
- // request to go in the queue because we can already process it as a
- // failure.
- // 1. The user explictly wants to download this attachment from the
- // email view but they should not be able to...either because there is
- // no app to view it or because its been marked as a policy violation.
- // 2. The user is forwarding an email and the attachment has been
- // marked as a policy violation. If the attachment is non viewable
- // that is OK for forwarding a message so we'll let it pass through
- markAttachmentAsFailed(att);
- return;
- }
- // If we get this far it a forward of an attachment that is only
- // ineligible because we can't view it or process it. Not because we
- // can't download it for policy reasons. Let's let this go through because
- // the final recipient of this forward email might be able to process it.
- }
- 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 (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Download queued for attachment " + att.mId + ", class " +
- req.priority + ", priority time " + req.time);
- }
- }
- // Process the queue if we're in a wait
- kick();
- }
-
- /**
- * Find a queued DownloadRequest, given the attachment's id
- * @param id the id of the attachment
- * @return the DownloadRequest for that attachment (or null, if none)
- */
- /*package*/ synchronized DownloadRequest findDownloadRequest(long id) {
- Iterator<DownloadRequest> iterator = iterator();
- while(iterator.hasNext()) {
- DownloadRequest req = iterator.next();
- if (req.attachmentId == id) {
- return req;
- }
- }
- return null;
- }
-
- @Override
- public synchronized boolean isEmpty() {
- return super.isEmpty() && mDownloadsInProgress.isEmpty();
- }
-
- /**
- * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
- * the limit on maximum downloads
- */
- /*package*/ synchronized void processQueue() {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Checking attachment queue, " + mDownloadSet.size()
- + " entries");
- }
- Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
- // First, start up any required downloads, in priority order
- while (iterator.hasNext() &&
- (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) {
- DownloadRequest req = iterator.next();
- // Enforce per-account limit here
- if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" +
- req.accountId);
- }
- continue;
- } else if (Attachment.restoreAttachmentWithId(mContext, req.attachmentId) == null) {
- continue;
- }
- if (!req.inProgress) {
- final long currentTime = SystemClock.elapsedRealtime();
- if (req.retryCount > 0 && req.retryStartTime > currentTime) {
- LogUtils.d(TAG, "== waiting to retry attachment %d", req.attachmentId);
- setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
- continue;
- }
- // TODO: We try to gate ineligible downloads from entering the queue but its
- // always possible that they made it in here regardless in the future. In a
- // perfect world, we would make it bullet proof with a check for eligibility
- // here instead/also.
- mDownloadSet.tryStartDownload(req);
- }
- }
-
- // Don't prefetch if background downloading is disallowed
- EmailConnectivityManager ecm = mConnectivityManager;
- if (ecm == null) return;
- if (!ecm.isAutoSyncAllowed()) return;
- // Don't prefetch unless we're on a WiFi network
- if (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI) {
- return;
- }
- // Then, try opportunistic download of appropriate attachments
- int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
- // Always leave one slot for user requested download
- if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) {
- // We'll load up the newest 25 attachments that aren't loaded or queued
- Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
- MAX_ATTACHMENTS_TO_CHECK);
- Cursor c = mContext.getContentResolver().query(lookupUri,
- Attachment.CONTENT_PROJECTION,
- EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
- null, AttachmentColumns._ID + " DESC");
- File cacheDir = mContext.getCacheDir();
- try {
- while (c.moveToNext()) {
- Attachment att = new Attachment();
- att.restore(c);
- Account account = Account.restoreAccountWithId(mContext, 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);
- } else {
- // Check that the attachment meets system requirements for download
- AttachmentInfo info = new AttachmentInfo(mContext, att);
- if (info.isEligibleForDownload()) {
- // Either the account must be able to prefetch or this must be
- // an inline attachment
- if (att.mContentId != null ||
- (canPrefetchForAccount(account, cacheDir))) {
- Integer tryCount;
- tryCount = mAttachmentFailureMap.get(att.mId);
- if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
- // move onto the next attachment
- continue;
- }
- // Start this download and we're done
- DownloadRequest req = new DownloadRequest(mContext, att);
- mDownloadSet.tryStartDownload(req);
- break;
- }
- } else {
- // If this attachment was ineligible for download
- // because of policy related issues, its flags would be set to
- // FLAG_POLICY_DISALLOWS_DOWNLOAD and would not show up in the
- // query results. We are most likely here for other reasons such
- // as the inability to view the attachment. In that case, let's just
- // skip it for now.
- LogUtils.e(TAG, "== skip attachment %d, it is ineligible", att.mId);
- }
- }
- }
- } finally {
- c.close();
- }
- }
- }
-
- /**
- * Count the number of running downloads in progress for this account
- * @param accountId the id of the account
- * @return the count of running downloads
- */
- /*package*/ synchronized int downloadsForAccount(long accountId) {
- int count = 0;
- for (DownloadRequest req: mDownloadsInProgress.values()) {
- if (req.accountId == accountId) {
- count++;
- }
- }
- return count;
- }
-
- /**
- * Watchdog for downloads; we use this in case we are hanging on a download, which might
- * have failed silently (the connection dropped, for example)
- */
- private void onWatchdogAlarm() {
- // If our service instance is gone, just leave
- if (mStop) {
- return;
- }
- long now = System.currentTimeMillis();
- for (DownloadRequest req: mDownloadsInProgress.values()) {
- // Check how long it's been since receiving a callback
- long timeSinceCallback = now - req.lastCallbackTime;
- if (timeSinceCallback > CALLBACK_TIMEOUT) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Download of " + req.attachmentId + " timed out");
- }
- cancelDownload(req);
- }
- }
- // Check whether we can start new downloads...
- if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) {
- processQueue();
- }
- // If there are downloads in progress, reset alarm
- if (!mDownloadsInProgress.isEmpty()) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "Reschedule watchdog...");
- }
- setWatchdogAlarm();
- }
- }
-
- /**
- * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
- * parameter
- * @param req the DownloadRequest
- * @return whether or not the download was started
- */
- /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) {
- EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
- AttachmentDownloadService.this, req.accountId);
-
- // Do not download the same attachment multiple times
- boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null;
- if (alreadyInProgress) return false;
-
- try {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, ">> Starting download for attachment #" + req.attachmentId);
- }
- startDownload(service, req);
- } catch (RemoteException e) {
- // TODO: Consider whether we need to do more in this case...
- // For now, fix up our data to reflect the failure
- cancelDownload(req);
- }
- return true;
- }
-
- private synchronized DownloadRequest getDownloadInProgress(long attachmentId) {
- return mDownloadsInProgress.get(attachmentId);
- }
-
- private void setWatchdogAlarm(final long delay) {
- // Lazily initialize the pending intent
- if (mWatchdogPendingIntent == null) {
- Intent intent = new Intent(mContext, Watchdog.class);
- mWatchdogPendingIntent =
- PendingIntent.getBroadcast(mContext, 0, intent, 0);
- }
- // Set the alarm
- AlarmManager am = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
- am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
- mWatchdogPendingIntent);
- }
-
- private void setWatchdogAlarm() {
- setWatchdogAlarm(WATCHDOG_CHECK_INTERVAL);
- }
-
- /**
- * Do the work of starting an attachment download using the EmailService interface, and
- * set our watchdog alarm
- *
- * @param service the service handling the download
- * @param req the DownloadRequest
- * @throws RemoteException
- */
- private void startDownload(EmailServiceProxy service, DownloadRequest req)
- throws RemoteException {
- req.startTime = System.currentTimeMillis();
- req.inProgress = true;
- mDownloadsInProgress.put(req.attachmentId, req);
- service.loadAttachment(mServiceCallback, req.accountId, req.attachmentId,
- req.priority != PRIORITY_FOREGROUND);
- setWatchdogAlarm();
- }
-
- private void cancelDownload(DownloadRequest req) {
- LogUtils.d(TAG, "cancelDownload #%d", req.attachmentId);
- req.inProgress = false;
- mDownloadsInProgress.remove(req.attachmentId);
- // Remove the download from our queue, and then decide whether or not to add it back.
- remove(req);
- req.retryCount++;
- if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
- LogUtils.d(TAG, "too many failures, giving up");
- } else {
- LogUtils.d(TAG, "moving to end of queue, will retry");
- // The time field of DownloadRequest is final, because it's unsafe to change it
- // as long as the DownloadRequest is in the DownloadSet. It's needed for the
- // comparator, so changing time would make the request unfindable.
- // Instead, we'll create a new DownloadRequest with an updated time.
- // This will sort at the end of the set.
- req = new DownloadRequest(req, SystemClock.elapsedRealtime());
- add(req);
- }
- }
-
- /**
- * Called when a download is finished; we get notified of this via our EmailServiceCallback
- * @param attachmentId the id of the attachment whose download is finished
- * @param statusCode the EmailServiceStatus code returned by the Service
- */
- /*package*/ synchronized void endDownload(long attachmentId, int statusCode) {
- // Say we're no longer downloading this
- mDownloadsInProgress.remove(attachmentId);
-
- // TODO: This code is conservative and treats connection issues as failures.
- // Since we have no mechanism to throttle reconnection attempts, it makes
- // sense to be cautious here. Once logic is in place to prevent connecting
- // in a tight loop, we can exclude counting connection issues as "failures".
-
- // Update the attachment failure list if needed
- Integer downloadCount;
- downloadCount = mAttachmentFailureMap.remove(attachmentId);
- if (statusCode != EmailServiceStatus.SUCCESS) {
- if (downloadCount == null) {
- downloadCount = 0;
- }
- downloadCount += 1;
- mAttachmentFailureMap.put(attachmentId, downloadCount);
- }
-
- DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
- if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
- // If this needs to be retried, just process the queue again
- if (req != null) {
- req.retryCount++;
- if (req.retryCount > CONNECTION_ERROR_MAX_RETRIES) {
- LogUtils.d(TAG, "Connection Error #%d, giving up", attachmentId);
- remove(req);
- } else if (req.retryCount > CONNECTION_ERROR_DELAY_THRESHOLD) {
- // TODO: I'm not sure this is a great retry/backoff policy, but we're
- // afraid of changing behavior too much in case something relies upon it.
- // So now, for the first five errors, we'll retry immediately. For the next
- // five tries, we'll add a ten second delay between each. After that, we'll
- // give up.
- LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
- attachmentId, req.retryCount);
- req.inProgress = false;
- req.retryStartTime = SystemClock.elapsedRealtime() +
- CONNECTION_ERROR_RETRY_MILLIS;
- setWatchdogAlarm(CONNECTION_ERROR_RETRY_MILLIS);
- } else {
- LogUtils.d(TAG, "ConnectionError #%d, retried %d times, adding delay",
- attachmentId, req.retryCount);
- req.inProgress = false;
- req.retryStartTime = 0;
- kick();
- }
- }
- return;
- }
-
- // If the request is still in the queue, remove it
- if (req != null) {
- remove(req);
- }
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- long secs = 0;
- if (req != null) {
- secs = (System.currentTimeMillis() - req.time) / 1000;
- }
- String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
- "Error " + statusCode;
- LogUtils.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs
- + " seconds from request, status: " + status);
- }
-
- Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
- if (attachment != null) {
- long accountId = attachment.mAccountKey;
- // Update our attachment storage for this account
- Long currentStorage = mAttachmentStorageMap.get(accountId);
- if (currentStorage == null) {
- currentStorage = 0L;
- }
- mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
- 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
- // can't be downloaded) delete it from the outgoing message, lest that
- // message never get sent
- EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId);
- // TODO: Talk to UX about whether this is even worth doing
- NotificationController nc = NotificationController.getInstance(mContext);
- 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)) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "== Downloads finished for outgoing msg #"
- + req.messageId);
- }
- EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
- mContext, accountId);
- try {
- service.sendMail(accountId);
- } catch (RemoteException e) {
- // We tried
- }
- }
- }
- if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
- Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey);
- if (msg == null) {
- // If there's no associated message, delete the attachment
- EmailContent.delete(mContext, 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?
- kick();
- return;
- }
- } else if (!deleted) {
- // Clear the download flags, since we're done for now. Note that this happens
- // only for non-recoverable errors. When these occur for forwarded mail, we can
- // ignore it and continue; otherwise, it was either 1) a user request, in which
- // case the user can retry manually or 2) an opportunistic download, in which
- // case the download wasn't critical
- ContentValues cv = new ContentValues();
- int flags =
- 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);
- }
- }
- // Process the queue
- kick();
- }
- }
-
- /**
- * 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_SEND_MAIL;
- } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
- priorityClass = PRIORITY_FOREGROUND;
- }
- 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.
- */
- private class ServiceCallback extends IEmailServiceCallback.Stub {
- @Override
- public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
- int progress) {
- // Record status and progress
- DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId);
- if (req != null) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- 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(TAG, ">> Attachment status " + attachmentId + ": " + code);
- } else if (progress >= (req.lastProgress + 10)) {
- LogUtils.d(TAG, ">> Attachment progress %d: %d%%", attachmentId, progress);
- }
- }
- req.lastStatusCode = statusCode;
- req.lastProgress = progress;
- req.lastCallbackTime = System.currentTimeMillis();
- Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId);
- if (attachment != null && statusCode == EmailServiceStatus.IN_PROGRESS) {
- 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:
- mDownloadSet.endDownload(attachmentId, statusCode);
- break;
- }
- }
- }
-
- /*package*/ void addServiceIntentForTest(long accountId, Intent intent) {
- mAccountServiceMap.put(accountId, intent);
- }
-
- /*package*/ void onChange(Attachment att) {
- mDownloadSet.onChange(this, att);
- }
-
- /*package*/ boolean isQueued(long attachmentId) {
- return mDownloadSet.findDownloadRequest(attachmentId) != null;
- }
-
- /*package*/ int getSize() {
- return mDownloadSet.size();
- }
-
- /*package*/ boolean dequeue(long attachmentId) {
- DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId);
- if (req != null) {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, "Dequeued attachmentId: " + attachmentId);
- }
- mDownloadSet.remove(req);
- return true;
- }
- return false;
- }
-
- /**
- * Ask the service for the number of items in the download queue
- * @return the number of items queued for download
- */
- public static int getQueueSize() {
- AttachmentDownloadService service = sRunningService;
- if (service != null) {
- return service.getSize();
- }
- return 0;
- }
-
- /**
- * Ask the service whether a particular attachment is queued for download
- * @param attachmentId the id of the Attachment (as stored by EmailProvider)
- * @return whether or not the attachment is queued for download
- */
- public static boolean isAttachmentQueued(long attachmentId) {
- AttachmentDownloadService service = sRunningService;
- if (service != null) {
- return service.isQueued(attachmentId);
- }
- return false;
- }
-
- /**
- * Ask the service to remove an attachment from the download queue
- * @param attachmentId the id of the Attachment (as stored by EmailProvider)
- * @return whether or not the attachment was removed from the queue
- */
- public static boolean cancelQueuedAttachment(long attachmentId) {
- AttachmentDownloadService service = sRunningService;
- if (service != null) {
- return service.dequeue(attachmentId);
- }
- return false;
- }
-
- public static void watchdogAlarm() {
- AttachmentDownloadService service = sRunningService;
- if (service != null) {
- service.mDownloadSet.onWatchdogAlarm();
- }
- }
-
- // 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
- * @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, AttachmentDownloadService.class);
- intent.putExtra(EXTRA_ATTACHMENT, attachment);
- context.startService(intent);
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- }
- }
-
- /**
- * Determine whether an attachment can be prefetched for the given account
- * @return true if download is allowed, false otherwise
- */
- public boolean canPrefetchForAccount(Account account, File dir) {
- // Check account, just in case
- if (account == null) return false;
- // First, check preference and quickly return if prefetch isn't allowed
- if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false;
-
- long totalStorage = dir.getTotalSpace();
- long usableStorage = dir.getUsableSpace();
- long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
-
- // If there's not enough overall storage available, stop now
- if (usableStorage < minAvailable) {
- return false;
- }
-
- int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
- long perAccountMaxStorage =
- (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
- // account, we walk the directory to determine the actual number
- Long accountStorage = mAttachmentStorageMap.get(account.mId);
- if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
- // Calculate the exact figure for attachment storage for this account
- accountStorage = 0L;
- File[] files = dir.listFiles();
- if (files != null) {
- for (File file : files) {
- accountStorage += file.length();
- }
- }
- // Cache the value
- mAttachmentStorageMap.put(account.mId, accountStorage);
- }
-
- // Return true if we're using less than the maximum per account
- if (accountStorage < perAccountMaxStorage) {
- return true;
- } else {
- if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
- LogUtils.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " +
- accountStorage + ", limit " + perAccountMaxStorage);
- }
- return false;
- }
- }
-
- @Override
- public void run() {
- // These fields are only used within the service thread
- mContext = this;
- mConnectivityManager = new EmailConnectivityManager(this, TAG);
- mAccountManagerStub = new AccountManagerStub(this);
-
- // 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;
- Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
- EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
- new String[] {Integer.toString(mask)}, null);
- try {
- LogUtils.d(TAG, "Count: " + c.getCount());
- while (c.moveToNext()) {
- Attachment attachment = Attachment.restoreAttachmentWithId(
- this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
- if (attachment != null) {
- mDownloadSet.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
- break;
- }
- mDownloadSet.processQueue();
- if (mDownloadSet.isEmpty()) {
- LogUtils.d(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(Intent intent, int flags, int startId) {
- if (sRunningService == null) {
- sRunningService = this;
- }
- if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) {
- Attachment att = intent.getParcelableExtra(EXTRA_ATTACHMENT);
- onChange(att);
- }
- return Service.START_STICKY;
- }
-
- @Override
- public void onCreate() {
- // Start up our service thread
- new Thread(this, "AttachmentDownloadService").start();
- }
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public void onDestroy() {
- // Mark this instance of the service as stopped
- mStop = true;
- if (sRunningService != null) {
- kick();
- sRunningService = null;
- }
- if (mConnectivityManager != null) {
- mConnectivityManager.unregister();
- mConnectivityManager.stopWait();
- mConnectivityManager = null;
- }
- }
-
- @Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println("AttachmentDownloadService");
- long time = System.currentTimeMillis();
- synchronized(mDownloadSet) {
- pw.println(" Queue, " + mDownloadSet.size() + " entries");
- Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator();
- // First, start up any required downloads, in priority order
- while (iterator.hasNext()) {
- DownloadRequest req = iterator.next();
- pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId);
- pw.println(" Priority: " + req.priority + ", Time: " + req.time +
- (req.inProgress ? " [In progress]" : ""));
- Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId);
- if (att == null) {
- pw.println(" Attachment not in database?");
- } else if (att.mFileName != null) {
- String fileName = att.mFileName;
- String suffix = "[none]";
- int lastDot = fileName.lastIndexOf('.');
- if (lastDot >= 0) {
- suffix = fileName.substring(lastDot);
- }
- pw.print(" Suffix: " + suffix);
- if (att.getContentUri() != null) {
- pw.print(" ContentUri: " + att.getContentUri());
- }
- pw.print(" Mime: ");
- if (att.mMimeType != null) {
- pw.print(att.mMimeType);
- } else {
- pw.print(AttachmentUtilities.inferMimeType(fileName, null));
- pw.print(" [inferred]");
- }
- pw.println(" Size: " + att.mSize);
- }
- if (req.inProgress) {
- pw.println(" Status: " + req.lastStatusCode + ", Progress: " +
- req.lastProgress);
- pw.println(" Started: " + req.startTime + ", Callback: " +
- req.lastCallbackTime);
- pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s");
- if (req.lastCallbackTime > 0) {
- pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s");
- }
- }
- }
- }
- }
-}
diff --git a/src/com/android/email/service/EmailServiceStub.java b/src/com/android/email/service/EmailServiceStub.java
index 2e20a93d6..81b4fc3c1 100644
--- a/src/com/android/email/service/EmailServiceStub.java
+++ b/src/com/android/email/service/EmailServiceStub.java
@@ -219,7 +219,7 @@ public abstract class EmailServiceStub extends IEmailService.Stub implements IEm
new MessageRetrievalListenerBridge(messageId, attachmentId, cb));
// If we failed to load the attachment, throw an Exception here, so that
- // AttachmentDownloadService knows that we failed
+ // AttachmentService knows that we failed
if (storePart.getBody() == null) {
throw new MessagingException("Attachment not loaded.");
}
diff --git a/src/com/android/email2/ui/MailActivityEmail.java b/src/com/android/email2/ui/MailActivityEmail.java
index 90364d9d1..517c928c6 100644
--- a/src/com/android/email2/ui/MailActivityEmail.java
+++ b/src/com/android/email2/ui/MailActivityEmail.java
@@ -29,7 +29,7 @@ import android.os.Bundle;
import com.android.email.NotificationController;
import com.android.email.Preferences;
import com.android.email.provider.EmailProvider;
-import com.android.email.service.AttachmentDownloadService;
+import com.android.email.service.AttachmentService;
import com.android.email.service.EmailServiceUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.TempDirectory;
@@ -124,13 +124,15 @@ public class MailActivityEmail extends com.android.mail.ui.MailActivity {
private static void setServicesEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
- new ComponentName(context, AttachmentDownloadService.class),
+ new ComponentName(context, AttachmentService.class),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
// Start/stop the various services depending on whether there are any accounts
- startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class));
+ // TODO: Make sure that the AttachmentService responds to this request as it
+ // expects a particular set of data in the intents that it receives or it ignores.
+ startOrStopService(enabled, context, new Intent(context, AttachmentService.class));
NotificationController.getInstance(context).watchForMessages();
}
diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java
index b7e692290..a1506fd71 100644
--- a/tests/src/com/android/email/provider/ProviderTests.java
+++ b/tests/src/com/android/email/provider/ProviderTests.java
@@ -34,7 +34,7 @@ import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.suitebuilder.annotation.Suppress;
-import com.android.email.provider.EmailProvider.AttachmentService;
+import com.android.email.provider.EmailProvider.EmailAttachmentService;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
@@ -110,7 +110,8 @@ public class ProviderTests extends ProviderTestCase2<EmailProvider> {
}
}
- private static final AttachmentService MOCK_ATTACHMENT_SERVICE = new AttachmentService() {
+ private static final EmailAttachmentService MOCK_ATTACHMENT_SERVICE =
+ new EmailAttachmentService() {
@Override
public void attachmentChanged(Context context, long id, int flags) {
// Noop. Don't download attachments.
diff --git a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java b/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java
deleted file mode 100644
index 32b68f7ae..000000000
--- a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * 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 android.content.Context;
-import android.test.suitebuilder.annotation.Suppress;
-
-import com.android.email.AccountTestCase;
-import com.android.email.EmailConnectivityManager;
-import com.android.email.provider.ProviderTestUtils;
-import com.android.email.service.AttachmentDownloadService.DownloadRequest;
-import com.android.email.service.AttachmentDownloadService.DownloadSet;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceStatus;
-
-import java.io.File;
-import java.util.Iterator;
-
-/**
- * Tests of the AttachmentDownloadService
- *
- * You can run this entire test case with:
- * runtest -c com.android.email.service.AttachmentDownloadServiceTests email
- */
-@Suppress
-public class AttachmentDownloadServiceTests extends AccountTestCase {
- private AttachmentDownloadService mService;
- private Context mMockContext;
- private Account mAccount;
- private Mailbox mMailbox;
- private long mAccountId;
- private long mMailboxId;
- private AttachmentDownloadService.AccountManagerStub mAccountManagerStub;
- private MockDirectory mMockDirectory;
-
- 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.mFlags |= Account.FLAGS_BACKGROUND_ATTACHMENTS;
- 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;
- // there's no NullEmailService class
- /*mService.addServiceIntentForTest(mAccountId, new Intent(mContext,
- NullEmailService.class));*/
- mAccountManagerStub = new AttachmentDownloadService.AccountManagerStub(null);
- mService.mAccountManagerStub = mAccountManagerStub;
- mService.mConnectivityManager = new MockConnectivityManager(mContext, "mock");
- mDownloadSet = mService.mDownloadSet;
- mMockDirectory =
- new MockDirectory(mService.mContext.getCacheDir().getAbsolutePath());
- }
-
- @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(mMockContext, att1);
- mDownloadSet.onChange(mMockContext, att2);
- mDownloadSet.onChange(mMockContext, att3);
- mDownloadSet.onChange(mMockContext, 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));
- }
-
- /**
- * A mock file directory containing a single (Mock)File. The total space, usable space, and
- * length of the single file can be set
- */
- private static class MockDirectory extends File {
- private static final long serialVersionUID = 1L;
- private long mTotalSpace;
- private long mUsableSpace;
- private MockFile[] mFiles;
- private final MockFile mMockFile = new MockFile();
-
-
- public MockDirectory(String path) {
- super(path);
- mFiles = new MockFile[1];
- mFiles[0] = mMockFile;
- }
-
- private void setTotalAndUsableSpace(long total, long usable) {
- mTotalSpace = total;
- mUsableSpace = usable;
- }
-
- @Override
- public long getTotalSpace() {
- return mTotalSpace;
- }
-
- @Override
- public long getUsableSpace() {
- return mUsableSpace;
- }
-
- public void setFileLength(long length) {
- mMockFile.mLength = length;
- }
-
- @Override
- public File[] listFiles() {
- return mFiles;
- }
- }
-
- /**
- * A mock file that reports back a pre-set length
- */
- private static class MockFile extends File {
- private static final long serialVersionUID = 1L;
- private long mLength = 0;
-
- public MockFile() {
- super("_mock");
- }
-
- @Override
- public long length() {
- return mLength;
- }
- }
-
- private static class MockConnectivityManager extends EmailConnectivityManager {
- public MockConnectivityManager(Context context, String name) {
- super(context, name);
- }
-
- @Override
- public void waitForConnectivity() {
- }
-
- @Override
- public boolean isAutoSyncAllowed() {
- return true;
- }
- }
-
- public void testCanPrefetchForAccount() {
- // First, test our "global" limits (based on free storage)
- // Mock storage @ 100 total and 26 available
- // Note that all file lengths in this test are in arbitrary units
- mMockDirectory.setTotalAndUsableSpace(100L, 26L);
- // Mock 2 accounts in total
- mAccountManagerStub.setNumberOfAccounts(2);
- // With 26% available, we should be ok to prefetch
- assertTrue(mService.canPrefetchForAccount(mAccount, mMockDirectory));
- // Now change to 24 available
- mMockDirectory.setTotalAndUsableSpace(100L, 24L);
- // With 24% available, we should NOT be ok to prefetch
- assertFalse(mService.canPrefetchForAccount(mAccount, mMockDirectory));
-
- // Now, test per-account storage
- // Mock storage @ 100 total and 50 available
- mMockDirectory.setTotalAndUsableSpace(100L, 50L);
- // Mock a file of length 12, but need to uncache previous amount first
- mService.mAttachmentStorageMap.remove(mAccountId);
- mMockDirectory.setFileLength(11);
- // We can prefetch since 11 < 50/4
- assertTrue(mService.canPrefetchForAccount(mAccount, mMockDirectory));
- // Mock a file of length 13, but need to uncache previous amount first
- mService.mAttachmentStorageMap.remove(mAccountId);
- mMockDirectory.setFileLength(13);
- // We can't prefetch since 13 > 50/4
- assertFalse(mService.canPrefetchForAccount(mAccount, mMockDirectory));
- }
-
- public void testCanPrefetchForAccountNoBackgroundDownload() {
- Account account = ProviderTestUtils.setupAccount("account2", false, mMockContext);
- account.mFlags &= ~Account.FLAGS_BACKGROUND_ATTACHMENTS;
- account.save(mMockContext);
-
- // First, test our "global" limits (based on free storage)
- // Mock storage @ 100 total and 26 available
- // Note that all file lengths in this test are in arbitrary units
- mMockDirectory.setTotalAndUsableSpace(100L, 26L);
- // Mock 2 accounts in total
- mAccountManagerStub.setNumberOfAccounts(2);
-
- // With 26% available, we should be ok to prefetch,
- // *but* bg download is disabled on the account.
- assertFalse(mService.canPrefetchForAccount(account, mMockDirectory));
- }
-}